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:
parent
c9aed1e6e2
commit
9522c59674
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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'];
|
||||||
|
@ -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 => ({
|
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),
|
||||||
},
|
},
|
||||||
|
@ -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}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
@ -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';
|
||||||
|
@ -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}
|
||||||
|
@ -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 { 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}
|
||||||
|
@ -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}
|
||||||
|
@ -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} />;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
@ -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 = (
|
@ -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',
|
@ -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';
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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}
|
||||||
|
@ -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]);
|
||||||
|
Loading…
Reference in New Issue
Block a user