1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

Refactor project health table (#1098)

* minor archive table updates

* archived date cell

* archive import paths

* move project health table files

* fix: align actions cells

* simplify health table row mapping

* fix project pages browser tab title

* initial draft of virtualized table component

* refactor: virtualized table common component

* fix: health report name cell width

* refactor: report cell paths
This commit is contained in:
Tymoteusz Czech 2022-06-21 09:08:37 +02:00 committed by GitHub
parent c9aed1e6e2
commit 9522c59674
27 changed files with 302 additions and 393 deletions

View File

@ -1,13 +1,6 @@
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
SortableTableHeader,
Table,
TableBody,
TableCell,
TablePlaceholder,
TableRow,
} from 'component/common/Table';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
@ -22,7 +15,6 @@ import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/Fe
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
import { ReviveArchivedFeatureCell } from 'component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell'; import { ReviveArchivedFeatureCell } from 'component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell';
import { useStyles } from 'component/feature/FeatureToggleList/styles';
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable'; import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { FeatureSchema } from 'openapi'; import { FeatureSchema } from 'openapi';
@ -31,7 +23,6 @@ import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { FeatureArchivedCell } from './FeatureArchivedCell/FeatureArchivedCell'; import { FeatureArchivedCell } from './FeatureArchivedCell/FeatureArchivedCell';
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
export interface IFeaturesArchiveTableProps { export interface IFeaturesArchiveTableProps {
@ -57,8 +48,6 @@ export const ArchiveTable = ({
title, title,
projectId, projectId,
}: IFeaturesArchiveTableProps) => { }: IFeaturesArchiveTableProps) => {
const rowHeight = theme.shape.tableRowHeight;
const { classes } = useStyles();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -107,7 +96,7 @@ export const ArchiveTable = ({
align: 'center', align: 'center',
}, },
{ {
Header: 'Feature toggle name', Header: 'Name',
accessor: 'name', accessor: 'name',
searchable: true, searchable: true,
minWidth: 100, minWidth: 100,
@ -209,8 +198,6 @@ export const ArchiveTable = ({
headerGroups, headerGroups,
rows, rows,
state: { sortBy }, state: { sortBy },
getTableBodyProps,
getTableProps,
prepareRow, prepareRow,
setHiddenColumns, setHiddenColumns,
} = useTable( } = useTable(
@ -256,15 +243,12 @@ export const ArchiveTable = ({
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps }, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
const [firstRenderedIndex, lastRenderedIndex] =
useVirtualizedRange(rowHeight);
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
header={ header={
<PageHeader <PageHeader
title={`${title} (${ titleElement={`${title} (${
rows.length < data.length rows.length < data.length
? `${rows.length} of ${data.length}` ? `${rows.length} of ${data.length}`
: data.length : data.length
@ -281,61 +265,11 @@ export const ArchiveTable = ({
} }
> >
<SearchHighlightProvider value={getSearchText(searchValue)}> <SearchHighlightProvider value={getSearchText(searchValue)}>
<Table <VirtualizedTable
{...getTableProps()} rows={rows}
rowHeight={rowHeight} headerGroups={headerGroups}
style={{ prepareRow={prepareRow}
height:
rowHeight * rows.length +
theme.shape.tableRowHeightCompact,
}}
>
<SortableTableHeader
headerGroups={headerGroups as any}
flex
/> />
<TableBody {...getTableBodyProps()}>
{rows.map((row, index) => {
const isVirtual =
index < firstRenderedIndex ||
index > lastRenderedIndex;
if (isVirtual) {
return null;
}
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
style={{
display: 'flex',
top:
index * rowHeight +
theme.shape.tableRowHeightCompact,
}}
className={classes.row}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps({
style: {
flex: cell.column.minWidth
? '1 0 auto'
: undefined,
},
})}
className={classes.cell}
>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider> </SearchHighlightProvider>
<ConditionallyRender <ConditionallyRender
condition={rows.length === 0} condition={rows.length === 0}

View File

@ -25,7 +25,7 @@ export const ProjectFeaturesArchiveTable = ({
return ( return (
<ArchiveTable <ArchiveTable
title="Project Features Archive" title="Project archive"
archivedFeatures={archivedFeatures} archivedFeatures={archivedFeatures}
loading={loading} loading={loading}
storedParams={value} storedParams={value}

View File

@ -26,7 +26,7 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
})); }));
interface IPageHeaderProps { interface IPageHeaderProps {
title: string; title?: string;
titleElement?: ReactNode; titleElement?: ReactNode;
subtitle?: string; subtitle?: string;
variant?: TypographyProps['variant']; variant?: TypographyProps['variant'];

View File

@ -0,0 +1,16 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(() => ({
row: {
position: 'absolute',
width: '100%',
},
cell: {
alignItems: 'center',
display: 'flex',
flexShrink: 0,
'& > *': {
flexGrow: 1,
},
},
}));

View File

@ -0,0 +1,101 @@
import { useMemo, VFC } from 'react';
import { useTheme } from '@mui/material';
import {
SortableTableHeader,
Table,
TableCell,
TableBody,
TableRow,
} from 'component/common/Table';
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
import { useStyles } from './VirtualizedTable.styles';
import { HeaderGroup, Row } from 'react-table';
interface IVirtualizedTableProps {
rowHeight?: number;
headerGroups: HeaderGroup<object>[];
rows: Row<object>[];
prepareRow: (row: Row) => void;
}
/**
* READ BEFORE USE
*
* Virtualized tables require some setup.
* With this component all but one columns are fixed width, and one fills remaining space.
* Add `maxWidth` to columns that will be static in width, and `minWidth` to the one that should grow.
*
* Remember to add `useFlexLayout` to `useTable`
* (more at: https://react-table-v7.tanstack.com/docs/api/useFlexLayout)
*/
export const VirtualizedTable: VFC<IVirtualizedTableProps> = ({
rowHeight: rowHeightOverride,
headerGroups,
rows,
prepareRow,
}) => {
const { classes } = useStyles();
const theme = useTheme();
const rowHeight = useMemo(
() => rowHeightOverride || theme.shape.tableRowHeight,
[rowHeightOverride, theme.shape.tableRowHeight]
);
const [firstRenderedIndex, lastRenderedIndex] =
useVirtualizedRange(rowHeight);
const tableHeight = useMemo(
() => rowHeight * rows.length + theme.shape.tableRowHeightCompact,
[rowHeight, rows.length, theme.shape.tableRowHeightCompact]
);
return (
<Table
role="table"
rowHeight={rowHeight}
style={{ height: tableHeight }}
>
<SortableTableHeader headerGroups={headerGroups} flex />
<TableBody role="rowgroup">
{rows.map((row, index) => {
const top =
index * rowHeight + theme.shape.tableRowHeightCompact;
const isVirtual =
index < firstRenderedIndex || index > lastRenderedIndex;
if (isVirtual) {
return null;
}
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
key={row.id}
className={classes.row}
style={{ display: 'flex', top }}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps({
style: {
flex: cell.column.minWidth
? '1 0 auto'
: undefined,
},
})}
className={classes.cell}
>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
);
};

View File

@ -3,7 +3,7 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
container: { container: {
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
padding: theme.spacing(0, 1.5), padding: theme.spacing(0, 1.5),
}, },

View File

@ -11,12 +11,10 @@ interface IFeatureNameCellProps {
}; };
} }
export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => { export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => (
return (
<LinkCell <LinkCell
title={row.original.name} title={row.original.name}
subtitle={row.original.description} subtitle={row.original.description}
to={`/projects/${row.original.project}/features/${row.original.name}`} to={`/projects/${row.original.project}/features/${row.original.name}`}
/> />
); );
};

View File

@ -3,3 +3,4 @@ export { TableBody, TableRow } from '@mui/material';
export { Table } from './Table/Table'; export { Table } from './Table/Table';
export { TableCell } from './TableCell/TableCell'; export { TableCell } from './TableCell/TableCell';
export { TablePlaceholder } from './TablePlaceholder/TablePlaceholder'; export { TablePlaceholder } from './TablePlaceholder/TablePlaceholder';
export { VirtualizedTable } from './VirtualizedTable/VirtualizedTable';

View File

@ -2,14 +2,7 @@ import { useEffect, useMemo, useState, VFC } from 'react';
import { Link, useMediaQuery, useTheme } from '@mui/material'; import { Link, useMediaQuery, useTheme } from '@mui/material';
import { Link as RouterLink, useSearchParams } from 'react-router-dom'; import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
Table,
SortableTableHeader,
TableBody,
TableCell,
TableRow,
TablePlaceholder,
} from 'component/common/Table';
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
@ -22,11 +15,9 @@ import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
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 { useStyles } from './styles';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
@ -108,8 +99,6 @@ const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
export const FeatureToggleListTable: VFC = () => { export const FeatureToggleListTable: VFC = () => {
const theme = useTheme(); const theme = useTheme();
const rowHeight = theme.shape.tableRowHeight;
const { classes } = useStyles();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const { features = [], loading } = useFeatures(); const { features = [], loading } = useFeatures();
@ -143,8 +132,6 @@ export const FeatureToggleListTable: VFC = () => {
); );
const { const {
getTableProps,
getTableBodyProps,
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
@ -191,12 +178,6 @@ export const FeatureToggleListTable: VFC = () => {
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
}, [sortBy, searchValue, setSearchParams]); }, [sortBy, searchValue, setSearchParams]);
const [firstRenderedIndex, lastRenderedIndex] =
useVirtualizedRange(rowHeight);
const tableHeight =
rowHeight * rows.length + theme.shape.tableRowHeightCompact;
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
@ -253,54 +234,11 @@ export const FeatureToggleListTable: VFC = () => {
} }
> >
<SearchHighlightProvider value={getSearchText(searchValue)}> <SearchHighlightProvider value={getSearchText(searchValue)}>
<Table <VirtualizedTable
{...getTableProps()} rows={rows}
rowHeight={rowHeight} headerGroups={headerGroups}
style={{ height: tableHeight }} prepareRow={prepareRow}
> />
<SortableTableHeader headerGroups={headerGroups} flex />
<TableBody {...getTableBodyProps()}>
{rows.map((row, index) => {
const top =
index * rowHeight +
theme.shape.tableRowHeightCompact;
const isVirtual =
index < firstRenderedIndex ||
index > lastRenderedIndex;
if (isVirtual) {
return null;
}
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
key={row.id}
className={classes.row}
style={{ display: 'flex', top }}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps({
style: {
flex: cell.column.minWidth
? '1 0 auto'
: undefined,
},
})}
className={classes.cell}
>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider> </SearchHighlightProvider>
<ConditionallyRender <ConditionallyRender
condition={rows.length === 0} condition={rows.length === 0}

View File

@ -1,50 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
actionsContainer: {
display: 'flex',
alignItems: 'center',
},
listParagraph: {
textAlign: 'center',
},
searchBarContainer: {
marginBottom: '2rem',
display: 'flex',
gap: '1rem',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down('sm')]: {
display: 'block',
},
'&.dense': {
marginBottom: '1rem',
},
},
searchBar: {
minWidth: '450px',
[theme.breakpoints.down('sm')]: {
minWidth: '100%',
},
},
emptyStateListItem: {
border: `2px dashed ${theme.palette.grey[100]}`,
padding: '0.8rem',
textAlign: 'center',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
row: {
position: 'absolute',
width: '100%',
},
cell: {
alignItems: 'center',
display: 'flex',
flexShrink: 0,
'& > *': {
flexGrow: 1,
},
},
}));

View File

@ -33,34 +33,55 @@ const Project = () => {
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const basePath = `/projects/${projectId}`; const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId;
const tabData = [ const tabData = [
{ {
title: 'Overview', title: 'Overview',
component: <ProjectOverview projectId={projectId} />, component: (
<ProjectOverview
projectId={projectId}
projectName={projectName}
/>
),
path: basePath, path: basePath,
name: 'overview', name: 'overview',
}, },
{ {
title: 'Health', title: 'Health',
component: <ProjectHealth projectId={projectId} />, component: (
<ProjectHealth
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/health`, path: `${basePath}/health`,
name: 'health', name: 'health',
}, },
{ {
title: 'Access', title: 'Access',
component: <ProjectAccess />, component: <ProjectAccess projectName={projectName} />,
path: `${basePath}/access`, path: `${basePath}/access`,
name: 'access', name: 'access',
}, },
{ {
title: 'Environments', title: 'Environments',
component: <ProjectEnvironment projectId={projectId} />, component: (
<ProjectEnvironment
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/environments`, path: `${basePath}/environments`,
name: 'environments', name: 'environments',
}, },
{ {
title: 'Archive', title: 'Archive',
component: <ProjectFeaturesArchive projectId={projectId} />, component: (
<ProjectFeaturesArchive
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/archive`, path: `${basePath}/archive`,
name: 'archive', name: 'archive',
}, },
@ -116,7 +137,7 @@ const Project = () => {
<div className={styles.innerContainer}> <div className={styles.innerContainer}>
<h2 className={styles.title}> <h2 className={styles.title}>
<div className={styles.titleText} data-loading> <div className={styles.titleText} data-loading>
{project?.name || projectId} {projectName}
</div> </div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}

View File

@ -18,18 +18,10 @@ import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/Fe
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { IProject } from 'interfaces/project'; import { IProject } from 'interfaces/project';
import { import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
Table,
SortableTableHeader,
TableBody,
TableCell,
TableRow,
TablePlaceholder,
} from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import useProject from 'hooks/api/getters/useProject/useProject'; import useProject from 'hooks/api/getters/useProject/useProject';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors'; import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog'; import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
@ -104,7 +96,6 @@ export const ProjectFeatureToggles = ({
); );
const { refetch } = useProject(projectId); const { refetch } = useProject(projectId);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const rowHeight = theme.shape.tableRowHeight;
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi(); useFeatureApi();
@ -282,7 +273,7 @@ export const ProjectFeatureToggles = ({
getSearchContext, getSearchContext,
} = useSearch(columns, searchValue, featuresData); } = useSearch(columns, searchValue, featuresData);
const data = useMemo<ListItemType[]>(() => { const data = useMemo<object[]>(() => {
if (loading) { if (loading) {
return Array(6).fill({ return Array(6).fill({
type: '-', type: '-',
@ -291,7 +282,7 @@ export const ProjectFeatureToggles = ({
environments: { environments: {
production: { name: 'production', enabled: false }, production: { name: 'production', enabled: false },
}, },
}) as ListItemType[]; }) as object[];
} }
return searchedData; return searchedData;
}, [loading, searchedData]); }, [loading, searchedData]);
@ -343,8 +334,6 @@ export const ProjectFeatureToggles = ({
headerGroups, headerGroups,
rows, rows,
state: { sortBy, hiddenColumns }, state: { sortBy, hiddenColumns },
getTableBodyProps,
getTableProps,
prepareRow, prepareRow,
setHiddenColumns, setHiddenColumns,
} = useTable( } = useTable(
@ -392,12 +381,6 @@ export const ProjectFeatureToggles = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]); }, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
const [firstRenderedIndex, lastRenderedIndex] =
useVirtualizedRange(rowHeight);
const tableHeight =
rowHeight * rows.length + theme.shape.tableRowHeightCompact;
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
@ -406,7 +389,7 @@ export const ProjectFeatureToggles = ({
header={ header={
<PageHeader <PageHeader
className={styles.title} className={styles.title}
title={`Feature toggles (${rows.length})`} titleElement={`Feature toggles (${rows.length})`}
actions={ actions={
<> <>
<ConditionallyRender <ConditionallyRender
@ -464,58 +447,11 @@ export const ProjectFeatureToggles = ({
} }
> >
<SearchHighlightProvider value={getSearchText(searchValue)}> <SearchHighlightProvider value={getSearchText(searchValue)}>
<Table <VirtualizedTable
{...getTableProps()} rows={rows}
rowHeight={rowHeight}
style={{ height: tableHeight }}
>
<SortableTableHeader
// @ts-expect-error -- verify after `react-table` v8
headerGroups={headerGroups} headerGroups={headerGroups}
className={styles.headerClass} prepareRow={prepareRow}
flex
/> />
<TableBody {...getTableBodyProps()}>
{rows.map((row, index) => {
const top =
index * rowHeight +
theme.shape.tableRowHeightCompact;
const isVirtual =
index < firstRenderedIndex ||
index > lastRenderedIndex;
if (isVirtual) {
return null;
}
prepareRow(row);
return (
<TableRow
hover
{...row.getRowProps()}
className={styles.row}
style={{ display: 'flex', top }}
>
{row.cells.map(cell => (
<TableCell
{...cell.getCellProps({
style: {
flex: cell.column.minWidth
? '1 0 auto'
: undefined,
},
})}
className={styles.cell}
>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider> </SearchHighlightProvider>
<ConditionallyRender <ConditionallyRender
condition={rows.length === 0} condition={rows.length === 0}

View File

@ -3,12 +3,14 @@ import { usePageTitle } from 'hooks/usePageTitle';
interface IProjectFeaturesArchiveProps { interface IProjectFeaturesArchiveProps {
projectId: string; projectId: string;
projectName: string;
} }
export const ProjectFeaturesArchive = ({ export const ProjectFeaturesArchive = ({
projectId, projectId,
projectName,
}: IProjectFeaturesArchiveProps) => { }: IProjectFeaturesArchiveProps) => {
usePageTitle('Project Archived Features'); usePageTitle(`Project archive ${projectName}`);
return <ProjectFeaturesArchiveTable projectId={projectId} />; return <ProjectFeaturesArchiveTable projectId={projectId} />;
}; };

View File

@ -1,19 +1,22 @@
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 { ReportCard } from 'component/Reporting/ReportCard/ReportCard';
import { usePageTitle } from 'hooks/usePageTitle'; import { usePageTitle } from 'hooks/usePageTitle';
import { ReportTable } from 'component/Reporting/ReportTable/ReportTable'; import { ReportCard } from './ReportTable/ReportCard/ReportCard';
import { ReportTable } from './ReportTable/ReportTable';
interface IProjectHealthProps { interface IProjectHealthProps {
projectId: string; projectId: string;
projectName: string;
} }
const ProjectHealth = ({ projectId }: IProjectHealthProps) => { const ProjectHealth = ({ projectId, projectName }: IProjectHealthProps) => {
usePageTitle('Project health'); usePageTitle(`Project health ${projectName}`);
const { healthReport, refetchHealthReport, error } = const { healthReport, refetchHealthReport, error } = useHealthReport(
useHealthReport(projectId); projectId,
{ refreshInterval: 15 * 1000 }
);
if (!healthReport) { if (!healthReport) {
return null; return null;

View File

@ -1,6 +1,7 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { Typography, useTheme } from '@mui/material';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { IReportTableRow } from 'component/Reporting/ReportTable/ReportTable'; import { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
interface IReportExpiredCellProps { interface IReportExpiredCellProps {
@ -10,9 +11,17 @@ interface IReportExpiredCellProps {
} }
export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => { export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => {
const theme = useTheme();
if (row.original.expiredAt) { if (row.original.expiredAt) {
return <DateCell value={row.original.expiredAt} />; return <DateCell value={row.original.expiredAt} />;
} }
return <TextCell>N/A</TextCell>; return (
<TextCell>
<Typography variant="body2" color={theme.palette.text.secondary}>
N/A
</Typography>
</TextCell>
);
}; };

View File

@ -1,10 +1,6 @@
import { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
import { import { getDiffInDays, expired, toggleExpiryByTypeMap } from '../utils';
getDiffInDays,
expired,
toggleExpiryByTypeMap,
} from 'component/Reporting/utils';
import { subDays, parseISO } from 'date-fns'; import { subDays, parseISO } from 'date-fns';
export const formatExpiredAt = ( export const formatExpiredAt = (

View File

@ -2,7 +2,7 @@ import { VFC, ReactElement } from 'react';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { ReportProblemOutlined, Check } from '@mui/icons-material'; import { ReportProblemOutlined, Check } from '@mui/icons-material';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { IReportTableRow } from 'component/Reporting/ReportTable/ReportTable'; import { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
const StyledTextPotentiallyStale = styled('span')(({ theme }) => ({ const StyledTextPotentiallyStale = styled('span')(({ theme }) => ({
display: 'flex', display: 'flex',

View File

@ -1,5 +1,5 @@
import { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { getDiffInDays, expired } from 'component/Reporting/utils'; import { getDiffInDays, expired } from '../utils';
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';

View File

@ -1,31 +1,36 @@
import { useMemo, useEffect } from 'react';
import { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { import {
SortableTableHeader, SortableTableHeader,
Table,
TableBody,
TableCell, TableCell,
TablePlaceholder, TablePlaceholder,
TableRow,
VirtualizedTable,
} from 'component/common/Table'; } from 'component/common/Table';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { useSortBy, useGlobalFilter, useTable } from 'react-table'; import {
import { Table, TableBody, TableRow, useMediaQuery } from '@mui/material'; useSortBy,
useGlobalFilter,
useTable,
useFlexLayout,
} from 'react-table';
import { useMediaQuery, useTheme } from '@mui/material';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; 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 { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
import theme from 'themes/theme';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import { ReportExpiredCell } from './ReportExpiredCell/ReportExpiredCell';
import { ReportStatusCell } from './ReportStatusCell/ReportStatusCell';
import { formatStatus, ReportingStatus } from './ReportStatusCell/formatStatus';
import { formatExpiredAt } from './ReportExpiredCell/formatExpiredAt';
interface IReportTableProps { interface IReportTableProps {
projectId: string; projectId: string;
@ -44,13 +49,25 @@ export interface IReportTableRow {
} }
export const ReportTable = ({ projectId, features }: IReportTableProps) => { export const ReportTable = ({ projectId, features }: IReportTableProps) => {
const theme = useTheme();
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const data: IReportTableRow[] = useMemo(() => { const data: IReportTableRow[] = useMemo<IReportTableRow[]>(
return features.map(feature => { () =>
return createReportTableRow(projectId, feature); features.map(report => ({
}); project: projectId,
}, [projectId, features]); name: report.name,
type: report.type,
stale: report.stale,
status: formatStatus(report),
lastSeenAt: report.lastSeenAt,
createdAt: report.createdAt,
expiredAt: formatExpiredAt(report),
})),
[projectId, features]
);
const initialState = useMemo( const initialState = useMemo(
() => ({ () => ({
@ -61,8 +78,6 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
); );
const { const {
getTableProps,
getTableBodyProps,
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
@ -80,20 +95,29 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
disableSortRemove: true, disableSortRemove: true,
}, },
useGlobalFilter, useGlobalFilter,
useFlexLayout,
useSortBy useSortBy
); );
useEffect(() => { useEffect(() => {
const hiddenColumns = []; const hiddenColumns = [];
if (isMediumScreen) {
hiddenColumns.push('createdAt');
}
if (isSmallScreen) { if (isSmallScreen) {
hiddenColumns.push('createdAt', 'expiredAt'); hiddenColumns.push('expiredAt', 'lastSeenAt');
}
if (isExtraSmallScreen) {
hiddenColumns.push('stale');
} }
setHiddenColumns(hiddenColumns); setHiddenColumns(hiddenColumns);
}, [setHiddenColumns, isSmallScreen]); }, [setHiddenColumns, isSmallScreen, isMediumScreen, isExtraSmallScreen]);
const header = ( return (
<PageContent
header={
<PageHeader <PageHeader
title="Overview" titleElement="Overview"
actions={ actions={
<Search <Search
initialValue={globalFilter} initialValue={globalFilter}
@ -101,28 +125,14 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
/> />
} }
/> />
); }
>
return (
<PageContent header={header}>
<SearchHighlightProvider value={globalFilter}> <SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}> <VirtualizedTable
<SortableTableHeader headerGroups={headerGroups} /> headerGroups={headerGroups}
<TableBody {...getTableBodyProps()}> prepareRow={prepareRow}
{rows.map(row => { rows={rows}
prepareRow(row); />
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider> </SearchHighlightProvider>
<ConditionallyRender <ConditionallyRender
condition={rows.length === 0} condition={rows.length === 0}
@ -149,22 +159,6 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
); );
}; };
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 = [ const COLUMNS = [
{ {
Header: 'Seen', Header: 'Seen',
@ -173,7 +167,7 @@ const COLUMNS = [
align: 'center', align: 'center',
Cell: FeatureSeenCell, Cell: FeatureSeenCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 85, maxWidth: 85,
}, },
{ {
Header: 'Type', Header: 'Type',
@ -181,36 +175,36 @@ const COLUMNS = [
align: 'center', align: 'center',
Cell: FeatureTypeCell, Cell: FeatureTypeCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 85, maxWidth: 85,
}, },
{ {
Header: 'Name', Header: 'Name',
accessor: 'name', accessor: 'name',
width: '60%',
sortType: 'alphanumeric', sortType: 'alphanumeric',
Cell: FeatureNameCell, Cell: FeatureNameCell,
minWidth: 120,
}, },
{ {
Header: 'Created on', Header: 'Created',
accessor: 'createdAt', accessor: 'createdAt',
sortType: 'date', sortType: 'date',
Cell: DateCell, Cell: DateCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 150, maxWidth: 150,
}, },
{ {
Header: 'Expired', Header: 'Expired',
accessor: 'expiredAt', accessor: 'expiredAt',
Cell: ReportExpiredCell, Cell: ReportExpiredCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 150, maxWidth: 150,
}, },
{ {
Header: 'Status', Header: 'Status',
id: 'status', id: 'status',
Cell: ReportStatusCell, Cell: ReportStatusCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 200, width: 180,
}, },
{ {
Header: 'State', Header: 'State',
@ -218,6 +212,6 @@ const COLUMNS = [
sortType: 'boolean', sortType: 'boolean',
Cell: FeatureStaleCell, Cell: FeatureStaleCell,
disableGlobalFilter: true, disableGlobalFilter: true,
minWidth: 120, maxWidth: 120,
}, },
]; ];

View File

@ -2,17 +2,20 @@ import useProject from 'hooks/api/getters/useProject/useProject';
import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles'; import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
import ProjectInfo from './ProjectInfo/ProjectInfo'; import ProjectInfo from './ProjectInfo/ProjectInfo';
import { useStyles } from './Project.styles'; import { useStyles } from './Project.styles';
import { usePageTitle } from 'hooks/usePageTitle';
interface IProjectOverviewProps { interface IProjectOverviewProps {
projectName: string;
projectId: string; projectId: string;
} }
const ProjectOverview = ({ projectId }: IProjectOverviewProps) => { const ProjectOverview = ({ projectId, projectName }: IProjectOverviewProps) => {
const { project, loading } = useProject(projectId, { const { project, loading } = useProject(projectId, {
refreshInterval: 15 * 1000, // ms refreshInterval: 15 * 1000, // ms
}); });
const { members, features, health, description, environments } = project; const { members, features, health, description, environments } = project;
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
usePageTitle(`Project overview ${projectName}`);
return ( return (
<div> <div>

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react'; import React, { useContext, VFC } from 'react';
import { ProjectAccessPage } from 'component/project/ProjectAccess/ProjectAccessPage'; import { ProjectAccessPage } from 'component/project/ProjectAccess/ProjectAccessPage';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -7,11 +7,17 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
export const ProjectAccess = () => { interface IProjectAccess {
projectName: string;
}
export const ProjectAccess: VFC<IProjectAccess> = ({ projectName }) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
usePageTitle(`Project access ${projectName}`);
if (isOss()) { if (isOss()) {
return ( return (

View File

@ -70,7 +70,7 @@ export const ProjectAccessPage = () => {
return ( return (
<PageContent <PageContent
header={<PageHeader title="Project roles" />} header={<PageHeader titleElement="Project roles" />}
className={styles.pageContent} className={styles.pageContent}
> >
<ProjectAccessAddUser roles={access?.roles} /> <ProjectAccessAddUser roles={access?.roles} />

View File

@ -7,7 +7,7 @@ import {
TableCell, TableCell,
SortableTableHeader, SortableTableHeader,
} from 'component/common/Table'; } from 'component/common/Table';
import { Avatar, Box, SelectChangeEvent } from '@mui/material'; import { Avatar, SelectChangeEvent } from '@mui/material';
import { Delete } from '@mui/icons-material'; import { Delete } from '@mui/icons-material';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { import {
@ -18,6 +18,7 @@ import { ProjectRoleCell } from './ProjectRoleCell/ProjectRoleCell';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
const initialState = { const initialState = {
sortBy: [{ id: 'name' }], sortBy: [{ id: 'name' }],
@ -94,16 +95,10 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
align: 'center', align: 'center',
width: 80, width: 80,
Cell: ({ row: { original: user } }: any) => ( Cell: ({ row: { original: user } }: any) => (
<Box <ActionCell>
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
projectId={projectId} projectId={projectId}
edge="end"
onClick={() => handleRemoveAccess(user)} onClick={() => handleRemoveAccess(user)}
disabled={access.users.length === 1} disabled={access.users.length === 1}
tooltipProps={{ tooltipProps={{
@ -115,7 +110,7 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
> >
<Delete /> <Delete />
</PermissionIconButton> </PermissionIconButton>
</Box> </ActionCell>
), ),
}, },
], ],

View File

@ -18,14 +18,18 @@ import { IProjectEnvironment } from 'interfaces/environments';
import { getEnabledEnvs } from './helpers'; import { getEnabledEnvs } from './helpers';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useThemeStyles } from 'themes/themeStyles'; import { useThemeStyles } from 'themes/themeStyles';
import { usePageTitle } from 'hooks/usePageTitle';
interface IProjectEnvironmentListProps { interface IProjectEnvironmentListProps {
projectId: string; projectId: string;
projectName: string;
} }
const ProjectEnvironmentList = ({ const ProjectEnvironmentList = ({
projectId, projectId,
projectName,
}: IProjectEnvironmentListProps) => { }: IProjectEnvironmentListProps) => {
usePageTitle(`Project environments ${projectName}`);
// api state // api state
const [envs, setEnvs] = useState<IProjectEnvironment[]>([]); const [envs, setEnvs] = useState<IProjectEnvironment[]>([]);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -176,7 +180,7 @@ const ProjectEnvironmentList = ({
<PageContent <PageContent
header={ header={
<PageHeader <PageHeader
title={`Configure environments for "${project?.name}" project`} titleElement={`Configure environments for "${project?.name}" project`}
/> />
} }
isLoading={loading} isLoading={loading}

View File

@ -1,18 +1,20 @@
import { useEffect, useContext } from 'react'; import { useEffect, useContext } from 'react';
import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext'; import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext';
export const usePageTitle = (title: string) => { export const usePageTitle = (title?: string) => {
const { setAnnouncement } = useContext(AnnouncerContext); const { setAnnouncement } = useContext(AnnouncerContext);
useEffect(() => { useEffect(() => {
if (title) {
document.title = title; document.title = title;
return () => { return () => {
document.title = DEFAULT_PAGE_TITLE; document.title = DEFAULT_PAGE_TITLE;
}; };
}
}, [title]); }, [title]);
useEffect(() => { useEffect(() => {
if (title !== DEFAULT_PAGE_TITLE) { if (title && title !== DEFAULT_PAGE_TITLE) {
setAnnouncement(`Navigated to ${title}`); setAnnouncement(`Navigated to ${title}`);
} }
}, [setAnnouncement, title]); }, [setAnnouncement, title]);