mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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:
parent
c9aed1e6e2
commit
9522c59674
@ -1,13 +1,6 @@
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import {
|
||||
SortableTableHeader,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TablePlaceholder,
|
||||
TableRow,
|
||||
} from 'component/common/Table';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
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 { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
|
||||
import { ReviveArchivedFeatureCell } from 'component/archive/ArchiveTable/ReviveArchivedFeatureCell/ReviveArchivedFeatureCell';
|
||||
import { useStyles } from 'component/feature/FeatureToggleList/styles';
|
||||
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||
import theme from 'themes/theme';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
@ -31,7 +23,6 @@ import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { FeatureArchivedCell } from './FeatureArchivedCell/FeatureArchivedCell';
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export interface IFeaturesArchiveTableProps {
|
||||
@ -57,8 +48,6 @@ export const ArchiveTable = ({
|
||||
title,
|
||||
projectId,
|
||||
}: IFeaturesArchiveTableProps) => {
|
||||
const rowHeight = theme.shape.tableRowHeight;
|
||||
const { classes } = useStyles();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -107,7 +96,7 @@ export const ArchiveTable = ({
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
Header: 'Feature toggle name',
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
searchable: true,
|
||||
minWidth: 100,
|
||||
@ -209,8 +198,6 @@ export const ArchiveTable = ({
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { sortBy },
|
||||
getTableBodyProps,
|
||||
getTableProps,
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
@ -256,15 +243,12 @@ export const ArchiveTable = ({
|
||||
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const [firstRenderedIndex, lastRenderedIndex] =
|
||||
useVirtualizedRange(rowHeight);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`${title} (${
|
||||
titleElement={`${title} (${
|
||||
rows.length < data.length
|
||||
? `${rows.length} of ${data.length}`
|
||||
: data.length
|
||||
@ -281,61 +265,11 @@ export const ArchiveTable = ({
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<Table
|
||||
{...getTableProps()}
|
||||
rowHeight={rowHeight}
|
||||
style={{
|
||||
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>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
|
@ -25,7 +25,7 @@ export const ProjectFeaturesArchiveTable = ({
|
||||
|
||||
return (
|
||||
<ArchiveTable
|
||||
title="Project Features Archive"
|
||||
title="Project archive"
|
||||
archivedFeatures={archivedFeatures}
|
||||
loading={loading}
|
||||
storedParams={value}
|
||||
|
@ -26,7 +26,7 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
interface IPageHeaderProps {
|
||||
title: string;
|
||||
title?: string;
|
||||
titleElement?: ReactNode;
|
||||
subtitle?: string;
|
||||
variant?: TypographyProps['variant'];
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}));
|
@ -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>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(0, 1.5),
|
||||
},
|
||||
|
@ -11,12 +11,10 @@ interface IFeatureNameCellProps {
|
||||
};
|
||||
}
|
||||
|
||||
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}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => (
|
||||
<LinkCell
|
||||
title={row.original.name}
|
||||
subtitle={row.original.description}
|
||||
to={`/projects/${row.original.project}/features/${row.original.name}`}
|
||||
/>
|
||||
);
|
||||
|
@ -3,3 +3,4 @@ export { TableBody, TableRow } from '@mui/material';
|
||||
export { Table } from './Table/Table';
|
||||
export { TableCell } from './TableCell/TableCell';
|
||||
export { TablePlaceholder } from './TablePlaceholder/TablePlaceholder';
|
||||
export { VirtualizedTable } from './VirtualizedTable/VirtualizedTable';
|
||||
|
@ -2,14 +2,7 @@ import { useEffect, useMemo, useState, VFC } from 'react';
|
||||
import { Link, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
|
||||
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
||||
import {
|
||||
Table,
|
||||
SortableTableHeader,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TablePlaceholder,
|
||||
} from 'component/common/Table';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
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 { sortTypes } from 'utils/sortTypes';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||
import { useStyles } from './styles';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
|
||||
@ -108,8 +99,6 @@ const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||
|
||||
export const FeatureToggleListTable: VFC = () => {
|
||||
const theme = useTheme();
|
||||
const rowHeight = theme.shape.tableRowHeight;
|
||||
const { classes } = useStyles();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const { features = [], loading } = useFeatures();
|
||||
@ -143,8 +132,6 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
@ -191,12 +178,6 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||
}, [sortBy, searchValue, setSearchParams]);
|
||||
|
||||
const [firstRenderedIndex, lastRenderedIndex] =
|
||||
useVirtualizedRange(rowHeight);
|
||||
|
||||
const tableHeight =
|
||||
rowHeight * rows.length + theme.shape.tableRowHeightCompact;
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
@ -253,54 +234,11 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<Table
|
||||
{...getTableProps()}
|
||||
rowHeight={rowHeight}
|
||||
style={{ height: tableHeight }}
|
||||
>
|
||||
<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>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}));
|
@ -33,34 +33,55 @@ const Project = () => {
|
||||
const { isOss } = useUiConfig();
|
||||
|
||||
const basePath = `/projects/${projectId}`;
|
||||
const projectName = project?.name || projectId;
|
||||
const tabData = [
|
||||
{
|
||||
title: 'Overview',
|
||||
component: <ProjectOverview projectId={projectId} />,
|
||||
component: (
|
||||
<ProjectOverview
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
),
|
||||
path: basePath,
|
||||
name: 'overview',
|
||||
},
|
||||
{
|
||||
title: 'Health',
|
||||
component: <ProjectHealth projectId={projectId} />,
|
||||
component: (
|
||||
<ProjectHealth
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
),
|
||||
path: `${basePath}/health`,
|
||||
name: 'health',
|
||||
},
|
||||
{
|
||||
title: 'Access',
|
||||
component: <ProjectAccess />,
|
||||
component: <ProjectAccess projectName={projectName} />,
|
||||
path: `${basePath}/access`,
|
||||
name: 'access',
|
||||
},
|
||||
{
|
||||
title: 'Environments',
|
||||
component: <ProjectEnvironment projectId={projectId} />,
|
||||
component: (
|
||||
<ProjectEnvironment
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
),
|
||||
path: `${basePath}/environments`,
|
||||
name: 'environments',
|
||||
},
|
||||
{
|
||||
title: 'Archive',
|
||||
component: <ProjectFeaturesArchive projectId={projectId} />,
|
||||
component: (
|
||||
<ProjectFeaturesArchive
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
),
|
||||
path: `${basePath}/archive`,
|
||||
name: 'archive',
|
||||
},
|
||||
@ -116,7 +137,7 @@ const Project = () => {
|
||||
<div className={styles.innerContainer}>
|
||||
<h2 className={styles.title}>
|
||||
<div className={styles.titleText} data-loading>
|
||||
{project?.name || projectId}
|
||||
{projectName}
|
||||
</div>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
|
@ -18,18 +18,10 @@ import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/Fe
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { IProject } from 'interfaces/project';
|
||||
import {
|
||||
Table,
|
||||
SortableTableHeader,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TablePlaceholder,
|
||||
} from 'component/common/Table';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||
@ -104,7 +96,6 @@ export const ProjectFeatureToggles = ({
|
||||
);
|
||||
const { refetch } = useProject(projectId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const rowHeight = theme.shape.tableRowHeight;
|
||||
|
||||
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
||||
useFeatureApi();
|
||||
@ -282,7 +273,7 @@ export const ProjectFeatureToggles = ({
|
||||
getSearchContext,
|
||||
} = useSearch(columns, searchValue, featuresData);
|
||||
|
||||
const data = useMemo<ListItemType[]>(() => {
|
||||
const data = useMemo<object[]>(() => {
|
||||
if (loading) {
|
||||
return Array(6).fill({
|
||||
type: '-',
|
||||
@ -291,7 +282,7 @@ export const ProjectFeatureToggles = ({
|
||||
environments: {
|
||||
production: { name: 'production', enabled: false },
|
||||
},
|
||||
}) as ListItemType[];
|
||||
}) as object[];
|
||||
}
|
||||
return searchedData;
|
||||
}, [loading, searchedData]);
|
||||
@ -343,8 +334,6 @@ export const ProjectFeatureToggles = ({
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { sortBy, hiddenColumns },
|
||||
getTableBodyProps,
|
||||
getTableProps,
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
@ -392,12 +381,6 @@ export const ProjectFeatureToggles = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
|
||||
|
||||
const [firstRenderedIndex, lastRenderedIndex] =
|
||||
useVirtualizedRange(rowHeight);
|
||||
|
||||
const tableHeight =
|
||||
rowHeight * rows.length + theme.shape.tableRowHeightCompact;
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
@ -406,7 +389,7 @@ export const ProjectFeatureToggles = ({
|
||||
header={
|
||||
<PageHeader
|
||||
className={styles.title}
|
||||
title={`Feature toggles (${rows.length})`}
|
||||
titleElement={`Feature toggles (${rows.length})`}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
@ -464,58 +447,11 @@ export const ProjectFeatureToggles = ({
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<Table
|
||||
{...getTableProps()}
|
||||
rowHeight={rowHeight}
|
||||
style={{ height: tableHeight }}
|
||||
>
|
||||
<SortableTableHeader
|
||||
// @ts-expect-error -- verify after `react-table` v8
|
||||
headerGroups={headerGroups}
|
||||
className={styles.headerClass}
|
||||
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>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
|
@ -3,12 +3,14 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
||||
|
||||
interface IProjectFeaturesArchiveProps {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export const ProjectFeaturesArchive = ({
|
||||
projectId,
|
||||
projectName,
|
||||
}: IProjectFeaturesArchiveProps) => {
|
||||
usePageTitle('Project Archived Features');
|
||||
usePageTitle(`Project archive – ${projectName}`);
|
||||
|
||||
return <ProjectFeaturesArchiveTable projectId={projectId} />;
|
||||
};
|
||||
|
@ -1,19 +1,22 @@
|
||||
import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import ApiError from 'component/common/ApiError/ApiError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ReportCard } from 'component/Reporting/ReportCard/ReportCard';
|
||||
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 {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
const ProjectHealth = ({ projectId }: IProjectHealthProps) => {
|
||||
usePageTitle('Project health');
|
||||
const ProjectHealth = ({ projectId, projectName }: IProjectHealthProps) => {
|
||||
usePageTitle(`Project health – ${projectName}`);
|
||||
|
||||
const { healthReport, refetchHealthReport, error } =
|
||||
useHealthReport(projectId);
|
||||
const { healthReport, refetchHealthReport, error } = useHealthReport(
|
||||
projectId,
|
||||
{ refreshInterval: 15 * 1000 }
|
||||
);
|
||||
|
||||
if (!healthReport) {
|
||||
return null;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { VFC } from 'react';
|
||||
import { Typography, useTheme } from '@mui/material';
|
||||
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';
|
||||
|
||||
interface IReportExpiredCellProps {
|
||||
@ -10,9 +11,17 @@ interface IReportExpiredCellProps {
|
||||
}
|
||||
|
||||
export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (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>
|
||||
);
|
||||
};
|
@ -1,10 +1,6 @@
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
|
||||
import {
|
||||
getDiffInDays,
|
||||
expired,
|
||||
toggleExpiryByTypeMap,
|
||||
} from 'component/Reporting/utils';
|
||||
import { getDiffInDays, expired, toggleExpiryByTypeMap } from '../utils';
|
||||
import { subDays, parseISO } from 'date-fns';
|
||||
|
||||
export const formatExpiredAt = (
|
@ -2,7 +2,7 @@ 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';
|
||||
import { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
|
||||
|
||||
const StyledTextPotentiallyStale = styled('span')(({ theme }) => ({
|
||||
display: 'flex',
|
@ -1,5 +1,5 @@
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { getDiffInDays, expired } from 'component/Reporting/utils';
|
||||
import { getDiffInDays, expired } from '../utils';
|
||||
import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
@ -1,31 +1,36 @@
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import {
|
||||
SortableTableHeader,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TablePlaceholder,
|
||||
TableRow,
|
||||
VirtualizedTable,
|
||||
} 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 {
|
||||
useSortBy,
|
||||
useGlobalFilter,
|
||||
useTable,
|
||||
useFlexLayout,
|
||||
} from 'react-table';
|
||||
import { useMediaQuery, useTheme } 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';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
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 {
|
||||
projectId: string;
|
||||
@ -44,13 +49,25 @@ export interface IReportTableRow {
|
||||
}
|
||||
|
||||
export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
||||
const theme = useTheme();
|
||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
|
||||
const data: IReportTableRow[] = useMemo(() => {
|
||||
return features.map(feature => {
|
||||
return createReportTableRow(projectId, feature);
|
||||
});
|
||||
}, [projectId, features]);
|
||||
const data: IReportTableRow[] = useMemo<IReportTableRow[]>(
|
||||
() =>
|
||||
features.map(report => ({
|
||||
project: projectId,
|
||||
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(
|
||||
() => ({
|
||||
@ -61,8 +78,6 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
@ -80,49 +95,44 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
||||
disableSortRemove: true,
|
||||
},
|
||||
useGlobalFilter,
|
||||
useFlexLayout,
|
||||
useSortBy
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hiddenColumns = [];
|
||||
if (isMediumScreen) {
|
||||
hiddenColumns.push('createdAt');
|
||||
}
|
||||
if (isSmallScreen) {
|
||||
hiddenColumns.push('createdAt', 'expiredAt');
|
||||
hiddenColumns.push('expiredAt', 'lastSeenAt');
|
||||
}
|
||||
if (isExtraSmallScreen) {
|
||||
hiddenColumns.push('stale');
|
||||
}
|
||||
setHiddenColumns(hiddenColumns);
|
||||
}, [setHiddenColumns, isSmallScreen]);
|
||||
|
||||
const header = (
|
||||
<PageHeader
|
||||
title="Overview"
|
||||
actions={
|
||||
<Search
|
||||
initialValue={globalFilter}
|
||||
onChange={setGlobalFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [setHiddenColumns, isSmallScreen, isMediumScreen, isExtraSmallScreen]);
|
||||
|
||||
return (
|
||||
<PageContent header={header}>
|
||||
<PageContent
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement="Overview"
|
||||
actions={
|
||||
<Search
|
||||
initialValue={globalFilter}
|
||||
onChange={setGlobalFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<VirtualizedTable
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
rows={rows}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
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 = [
|
||||
{
|
||||
Header: 'Seen',
|
||||
@ -173,7 +167,7 @@ const COLUMNS = [
|
||||
align: 'center',
|
||||
Cell: FeatureSeenCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 85,
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
@ -181,36 +175,36 @@ const COLUMNS = [
|
||||
align: 'center',
|
||||
Cell: FeatureTypeCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 85,
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: '60%',
|
||||
sortType: 'alphanumeric',
|
||||
Cell: FeatureNameCell,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
Header: 'Created on',
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
sortType: 'date',
|
||||
Cell: DateCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Expired',
|
||||
accessor: 'expiredAt',
|
||||
Cell: ReportExpiredCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
id: 'status',
|
||||
Cell: ReportStatusCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 200,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
Header: 'State',
|
||||
@ -218,6 +212,6 @@ const COLUMNS = [
|
||||
sortType: 'boolean',
|
||||
Cell: FeatureStaleCell,
|
||||
disableGlobalFilter: true,
|
||||
minWidth: 120,
|
||||
maxWidth: 120,
|
||||
},
|
||||
];
|
@ -2,17 +2,20 @@ import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
|
||||
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
||||
import { useStyles } from './Project.styles';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
|
||||
interface IProjectOverviewProps {
|
||||
projectName: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
const ProjectOverview = ({ projectId }: IProjectOverviewProps) => {
|
||||
const ProjectOverview = ({ projectId, projectName }: IProjectOverviewProps) => {
|
||||
const { project, loading } = useProject(projectId, {
|
||||
refreshInterval: 15 * 1000, // ms
|
||||
});
|
||||
const { members, features, health, description, environments } = project;
|
||||
const { classes: styles } = useStyles();
|
||||
usePageTitle(`Project overview – ${projectName}`);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, VFC } from 'react';
|
||||
import { ProjectAccessPage } from 'component/project/ProjectAccess/ProjectAccessPage';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
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 { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||
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 { hasAccess } = useContext(AccessContext);
|
||||
const { isOss } = useUiConfig();
|
||||
usePageTitle(`Project access – ${projectName}`);
|
||||
|
||||
if (isOss()) {
|
||||
return (
|
||||
|
@ -70,7 +70,7 @@ export const ProjectAccessPage = () => {
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
header={<PageHeader title="Project roles" />}
|
||||
header={<PageHeader titleElement="Project roles" />}
|
||||
className={styles.pageContent}
|
||||
>
|
||||
<ProjectAccessAddUser roles={access?.roles} />
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
TableCell,
|
||||
SortableTableHeader,
|
||||
} from 'component/common/Table';
|
||||
import { Avatar, Box, SelectChangeEvent } from '@mui/material';
|
||||
import { Avatar, SelectChangeEvent } from '@mui/material';
|
||||
import { Delete } from '@mui/icons-material';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import {
|
||||
@ -18,6 +18,7 @@ import { ProjectRoleCell } from './ProjectRoleCell/ProjectRoleCell';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
|
||||
const initialState = {
|
||||
sortBy: [{ id: 'name' }],
|
||||
@ -94,16 +95,10 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
|
||||
align: 'center',
|
||||
width: 80,
|
||||
Cell: ({ row: { original: user } }: any) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<ActionCell>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
projectId={projectId}
|
||||
edge="end"
|
||||
onClick={() => handleRemoveAccess(user)}
|
||||
disabled={access.users.length === 1}
|
||||
tooltipProps={{
|
||||
@ -115,7 +110,7 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</Box>
|
||||
</ActionCell>
|
||||
),
|
||||
},
|
||||
],
|
||||
|
@ -18,14 +18,18 @@ import { IProjectEnvironment } from 'interfaces/environments';
|
||||
import { getEnabledEnvs } from './helpers';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useThemeStyles } from 'themes/themeStyles';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
|
||||
interface IProjectEnvironmentListProps {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
const ProjectEnvironmentList = ({
|
||||
projectId,
|
||||
projectName,
|
||||
}: IProjectEnvironmentListProps) => {
|
||||
usePageTitle(`Project environments – ${projectName}`);
|
||||
// api state
|
||||
const [envs, setEnvs] = useState<IProjectEnvironment[]>([]);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -176,7 +180,7 @@ const ProjectEnvironmentList = ({
|
||||
<PageContent
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Configure environments for "${project?.name}" project`}
|
||||
titleElement={`Configure environments for "${project?.name}" project`}
|
||||
/>
|
||||
}
|
||||
isLoading={loading}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext';
|
||||
|
||||
export const usePageTitle = (title: string) => {
|
||||
export const usePageTitle = (title?: string) => {
|
||||
const { setAnnouncement } = useContext(AnnouncerContext);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = title;
|
||||
return () => {
|
||||
document.title = DEFAULT_PAGE_TITLE;
|
||||
};
|
||||
if (title) {
|
||||
document.title = title;
|
||||
return () => {
|
||||
document.title = DEFAULT_PAGE_TITLE;
|
||||
};
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (title !== DEFAULT_PAGE_TITLE) {
|
||||
if (title && title !== DEFAULT_PAGE_TITLE) {
|
||||
setAnnouncement(`Navigated to ${title}`);
|
||||
}
|
||||
}, [setAnnouncement, title]);
|
||||
|
Loading…
Reference in New Issue
Block a user