1
0
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:
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 { 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}

View File

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

View File

@ -26,7 +26,7 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
}));
interface IPageHeaderProps {
title: string;
title?: string;
titleElement?: ReactNode;
subtitle?: string;
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 => ({
container: {
display: 'flex',
justifyContent: 'flex-end',
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing(0, 1.5),
},

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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 (

View File

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

View File

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

View File

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

View File

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