mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
refactor: port segments list to react-table (#1024)
* refactor: extract SegmentEmpty component * refactor: extract CreateSegmentButton component * refactor: extract EditSegmentButton component * refactor: extract RemoveSegmentButton component * refactor: normalize Created table header text * refactor: port segments list to react-table * fix: improve row text height in table row * fix: update test snapshots * refactor table cell with search highlight * fix: update after review Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Co-authored-by: Tymoteusz Czech <tymek+gpg@getunleash.ai>
This commit is contained in:
parent
5bd840107e
commit
504a4af274
@ -1,30 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
tableRow: {
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.grey[200],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
leftTableCell: {
|
|
||||||
textAlign: 'left',
|
|
||||||
maxWidth: '300px',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'left',
|
|
||||||
maxWidth: '300px',
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hideSM: {
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hideXS: {
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,81 +0,0 @@
|
|||||||
import { useStyles } from './ProjectRoleListItem.styles';
|
|
||||||
import { TableCell, TableRow, Typography } from '@mui/material';
|
|
||||||
import { Delete, Edit } from '@mui/icons-material';
|
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { SupervisedUserCircle } from '@mui/icons-material';
|
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
|
||||||
import { IProjectRole } from 'interfaces/role';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface IRoleListItemProps {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
setCurrentRole: React.Dispatch<React.SetStateAction<IProjectRole>>;
|
|
||||||
setDelDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUILTIN_ROLE_TYPE = 'project';
|
|
||||||
|
|
||||||
const RoleListItem = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
description,
|
|
||||||
setCurrentRole,
|
|
||||||
setDelDialog,
|
|
||||||
}: IRoleListItemProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TableRow className={styles.tableRow}>
|
|
||||||
<TableCell className={styles.hideXS}>
|
|
||||||
<SupervisedUserCircle color="disabled" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.leftTableCell}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
{name}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.description}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
{description}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell align="right">
|
|
||||||
<PermissionIconButton
|
|
||||||
data-loading
|
|
||||||
disabled={type === BUILTIN_ROLE_TYPE}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/admin/roles/${id}/edit`);
|
|
||||||
}}
|
|
||||||
permission={ADMIN}
|
|
||||||
tooltipProps={{ title: 'Edit role' }}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</PermissionIconButton>
|
|
||||||
<PermissionIconButton
|
|
||||||
data-loading
|
|
||||||
disabled={type === BUILTIN_ROLE_TYPE}
|
|
||||||
onClick={() => {
|
|
||||||
// @ts-expect-error
|
|
||||||
setCurrentRole({ id, name, description });
|
|
||||||
setDelDialog(true);
|
|
||||||
}}
|
|
||||||
permission={ADMIN}
|
|
||||||
tooltipProps={{ title: 'Remove role' }}
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</PermissionIconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoleListItem;
|
|
@ -179,7 +179,7 @@ const UsersList = ({ search }: IUsersListProps) => {
|
|||||||
sort={sort}
|
sort={sort}
|
||||||
setSort={setSort}
|
setSort={setSort}
|
||||||
>
|
>
|
||||||
Created on
|
Created
|
||||||
</TableCellSortable>
|
</TableCellSortable>
|
||||||
<TableCell
|
<TableCell
|
||||||
align="center"
|
align="center"
|
||||||
|
@ -49,7 +49,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
|||||||
const paperProps = disableBorder ? { elevation: 0 } : {};
|
const paperProps = disableBorder ? { elevation: 0 } : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref} aria-busy={isLoading} aria-live="polite">
|
||||||
<Paper
|
<Paper
|
||||||
{...rest}
|
{...rest}
|
||||||
{...paperProps}
|
{...paperProps}
|
||||||
|
17
frontend/src/component/common/Table/Table/Table.styles.ts
Normal file
17
frontend/src/component/common/Table/Table/Table.styles.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles<{
|
||||||
|
rowHeight: 'auto' | 'standard' | 'dense' | 'compact' | number;
|
||||||
|
}>()((theme, { rowHeight }) => ({
|
||||||
|
table: {
|
||||||
|
'& tbody tr': {
|
||||||
|
height:
|
||||||
|
{
|
||||||
|
auto: 'auto',
|
||||||
|
standard: theme.shape.tableRowHeight,
|
||||||
|
compact: theme.shape.tableRowHeightCompact,
|
||||||
|
dense: theme.shape.tableRowHeightDense,
|
||||||
|
}[rowHeight] ?? rowHeight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
16
frontend/src/component/common/Table/Table/Table.tsx
Normal file
16
frontend/src/component/common/Table/Table/Table.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Table as MUITable, TableProps } from '@mui/material';
|
||||||
|
import { useStyles } from './Table.styles';
|
||||||
|
|
||||||
|
export const Table: FC<
|
||||||
|
TableProps & {
|
||||||
|
rowHeight?: 'auto' | 'dense' | 'standard' | 'compact' | number;
|
||||||
|
}
|
||||||
|
> = ({ rowHeight = 'auto', className, ...props }) => {
|
||||||
|
const { classes } = useStyles({ rowHeight });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MUITable className={classnames(classes.table, className)} {...props} />
|
||||||
|
);
|
||||||
|
};
|
@ -7,10 +7,7 @@ interface IContextActionsCellProps {
|
|||||||
|
|
||||||
export const ActionCell = ({ children }: IContextActionsCellProps) => {
|
export const ActionCell = ({ children }: IContextActionsCellProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}>
|
||||||
data-loading
|
|
||||||
sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -17,5 +17,5 @@ export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
|||||||
: formatDateYMD(parseISO(value), locationSettings.locale)
|
: formatDateYMD(parseISO(value), locationSettings.locale)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return <TextCell>{date}</TextCell>;
|
return <TextCell lineClamp={1}>{date}</TextCell>;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles()(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
padding: theme.spacing(1, 2),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
WebkitLineClamp: '1',
|
||||||
|
lineClamp: '1',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
WebkitLineClamp: '1',
|
||||||
|
lineClamp: '1',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
},
|
||||||
|
}));
|
@ -1,29 +1,48 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
import { useStyles } from './HighlightCell.styles';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface IHighlightCellProps {
|
interface IHighlightCellProps {
|
||||||
value?: string | null;
|
value: string;
|
||||||
children?: string | null;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HighlightCell: VFC<IHighlightCellProps> = ({
|
export const HighlightCell: VFC<IHighlightCellProps> = ({
|
||||||
value,
|
value,
|
||||||
children,
|
subtitle,
|
||||||
}) => {
|
}) => {
|
||||||
const { searchQuery } = useSearchHighlightContext();
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
const { classes } = useStyles();
|
||||||
const text = children ?? value;
|
|
||||||
if (!text) {
|
|
||||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 1.5, px: 2 }}>
|
<Box className={classes.container}>
|
||||||
<span data-loading role="tooltip">
|
<span
|
||||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
className={classes.title}
|
||||||
|
style={{
|
||||||
|
WebkitLineClamp: Boolean(subtitle) ? 1 : 2,
|
||||||
|
lineClamp: Boolean(subtitle) ? 1 : 2,
|
||||||
|
}}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
<Highlighter search={searchQuery}>{value}</Highlighter>
|
||||||
</span>
|
</span>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(subtitle)}
|
||||||
|
show={() => (
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
className={classes.subtitle}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
<Highlighter search={searchQuery}>
|
||||||
|
{subtitle}
|
||||||
|
</Highlighter>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,13 +8,11 @@ interface IIconCellProps {
|
|||||||
export const IconCell = ({ icon }: IIconCellProps) => {
|
export const IconCell = ({ icon }: IIconCellProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
data-loading
|
|
||||||
sx={{
|
sx={{
|
||||||
pl: 2,
|
pl: 2,
|
||||||
pr: 1,
|
pr: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: 60,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
@ -28,6 +28,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles<{ lineClamp?: number }>()(
|
||||||
|
(theme, { lineClamp }) => ({
|
||||||
|
wrapper: {
|
||||||
|
padding: theme.spacing(1, 2),
|
||||||
|
display: '-webkit-box',
|
||||||
|
overflow: lineClamp ? 'hidden' : 'auto',
|
||||||
|
WebkitLineClamp: lineClamp ? lineClamp : 'none',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
@ -1,21 +1,22 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
|
import { useStyles } from './TextCell.styles';
|
||||||
|
|
||||||
interface ITextCellProps {
|
interface ITextCellProps {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
|
lineClamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextCell: FC<ITextCellProps> = ({ value, children }) => {
|
export const TextCell: FC<ITextCellProps> = ({
|
||||||
const text = children ?? value;
|
value,
|
||||||
if (!text) {
|
children,
|
||||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
lineClamp,
|
||||||
}
|
}) => {
|
||||||
|
const { classes } = useStyles({ lineClamp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 1.5, px: 2 }}>
|
<Box className={classes.wrapper}>
|
||||||
<span data-loading role="tooltip">
|
<span data-loading>{children ?? value}</span>
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export { TableSearch } from './TableSearch/TableSearch';
|
export { TableSearch } from './TableSearch/TableSearch';
|
||||||
export { SortableTableHeader } from './SortableTableHeader/SortableTableHeader';
|
export { SortableTableHeader } from './SortableTableHeader/SortableTableHeader';
|
||||||
export { Table, TableBody, TableRow } from '@mui/material';
|
export { TableBody, TableRow } from '@mui/material';
|
||||||
|
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';
|
||||||
|
@ -112,8 +112,8 @@ const COLUMNS = [
|
|||||||
accessor: 'name',
|
accessor: 'name',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
canSort: false,
|
canSort: false,
|
||||||
Cell: (props: any) => (
|
Cell: ({ row: { original } }: any) => (
|
||||||
<EnvironmentNameCell environment={props.row.original} />
|
<EnvironmentNameCell environment={original} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -121,8 +121,8 @@ const COLUMNS = [
|
|||||||
id: 'Actions',
|
id: 'Actions',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
canSort: false,
|
canSort: false,
|
||||||
Cell: (props: any) => (
|
Cell: ({ row: { original } }: any) => (
|
||||||
<EnvironmentActionCell environment={props.row.original} />
|
<EnvironmentActionCell environment={original} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -68,7 +68,7 @@ const columns = [
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Created on',
|
Header: 'Created',
|
||||||
accessor: 'createdAt',
|
accessor: 'createdAt',
|
||||||
Cell: DateCell,
|
Cell: DateCell,
|
||||||
sortType: 'date',
|
sortType: 'date',
|
||||||
@ -236,7 +236,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider value={globalFilter}>
|
<SearchHighlightProvider value={globalFilter}>
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()} rowHeight={rowHeight}>
|
||||||
{/* @ts-expect-error -- fix in react-table v8 */}
|
{/* @ts-expect-error -- fix in react-table v8 */}
|
||||||
<SortableTableHeader headerGroups={headerGroups} flex />
|
<SortableTableHeader headerGroups={headerGroups} flex />
|
||||||
<TableBody
|
<TableBody
|
||||||
|
@ -36,7 +36,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
height: theme.shape.tableRowHeight,
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -49,9 +49,9 @@ import { SplashPage } from 'component/splash/SplashPage/SplashPage';
|
|||||||
import { CreateUnleashContextPage } from 'component/context/CreateUnleashContext/CreateUnleashContextPage';
|
import { CreateUnleashContextPage } from 'component/context/CreateUnleashContext/CreateUnleashContextPage';
|
||||||
import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
|
import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
|
||||||
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
||||||
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
|
|
||||||
import { IRoute } from 'interfaces/route';
|
import { IRoute } from 'interfaces/route';
|
||||||
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
||||||
|
import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
|
||||||
import RedirectAdminInvoices from 'component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices';
|
import RedirectAdminInvoices from 'component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
@ -348,7 +348,7 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/segments',
|
path: '/segments',
|
||||||
title: 'Segments',
|
title: 'Segments',
|
||||||
component: SegmentsList,
|
component: SegmentTable,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
|
@ -203,7 +203,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Created on',
|
Header: 'Created',
|
||||||
accessor: 'createdAt',
|
accessor: 'createdAt',
|
||||||
Cell: DateCell,
|
Cell: DateCell,
|
||||||
sortType: 'date',
|
sortType: 'date',
|
||||||
|
@ -142,7 +142,7 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()} rowHeight="compact">
|
||||||
{/* @ts-expect-error -- react-table */}
|
{/* @ts-expect-error -- react-table */}
|
||||||
<SortableTableHeader headerGroups={headerGroups} />
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
<TableBody {...getTableBodyProps()}>
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
@ -2,6 +2,8 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
cell: {
|
cell: {
|
||||||
padding: theme.spacing(1, 1.5),
|
padding: theme.spacing(0, 1.5),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { CREATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import { NAVIGATE_TO_CREATE_SEGMENT } from 'utils/testIds';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const CreateSegmentButton = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionButton
|
||||||
|
onClick={() => navigate('/segments/create')}
|
||||||
|
permission={CREATE_SEGMENT}
|
||||||
|
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
|
||||||
|
>
|
||||||
|
New segment
|
||||||
|
</PermissionButton>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
import { Edit } from '@mui/icons-material';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { UPDATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IEditSegmentButtonProps {
|
||||||
|
segment: ISegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditSegmentButton = ({ segment }: IEditSegmentButtonProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() => navigate(`/segments/edit/${segment.id}`)}
|
||||||
|
permission={UPDATE_SEGMENT}
|
||||||
|
tooltipProps={{ title: 'Edit segment' }}
|
||||||
|
>
|
||||||
|
<Edit data-loading />
|
||||||
|
</PermissionIconButton>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { DELETE_SEGMENT } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { Delete } from '@mui/icons-material';
|
||||||
|
import { SEGMENT_DELETE_BTN_ID } from 'utils/testIds';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { SegmentDelete } from 'component/segments/SegmentDelete/SegmentDelete';
|
||||||
|
import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface IRemoveSegmentButtonProps {
|
||||||
|
segment: ISegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RemoveSegmentButton = ({ segment }: IRemoveSegmentButtonProps) => {
|
||||||
|
const { refetchSegments } = useSegments();
|
||||||
|
const { deleteSegment } = useSegmentsApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const [showModal, toggleModal] = useState(false);
|
||||||
|
|
||||||
|
const onRemove = async () => {
|
||||||
|
try {
|
||||||
|
await deleteSegment(segment.id);
|
||||||
|
await refetchSegments();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Successfully deleted segment',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
} finally {
|
||||||
|
toggleModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() => toggleModal(true)}
|
||||||
|
permission={DELETE_SEGMENT}
|
||||||
|
tooltipProps={{ title: 'Remove segment' }}
|
||||||
|
data-testid={`${SEGMENT_DELETE_BTN_ID}_${segment.name}`}
|
||||||
|
>
|
||||||
|
<Delete data-loading />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showModal}
|
||||||
|
show={() => (
|
||||||
|
<SegmentDelete
|
||||||
|
segment={segment}
|
||||||
|
open={showModal}
|
||||||
|
onClose={() => toggleModal(false)}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
import { RemoveSegmentButton } from 'component/segments/RemoveSegmentButton/RemoveSegmentButton';
|
||||||
|
import { EditSegmentButton } from 'component/segments/EditSegmentButton/EditSegmentButton';
|
||||||
|
|
||||||
|
interface ISegmentActionCellProps {
|
||||||
|
segment: ISegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SegmentActionCell = ({ segment }: ISegmentActionCellProps) => {
|
||||||
|
return (
|
||||||
|
<ActionCell>
|
||||||
|
<EditSegmentButton segment={segment} />
|
||||||
|
<RemoveSegmentButton segment={segment} />
|
||||||
|
</ActionCell>
|
||||||
|
);
|
||||||
|
};
|
@ -8,14 +8,14 @@ import { SegmentDeleteUsedSegment } from './SegmentDeleteUsedSegment/SegmentDele
|
|||||||
interface ISegmentDeleteProps {
|
interface ISegmentDeleteProps {
|
||||||
segment: ISegment;
|
segment: ISegment;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
handleDeleteSegment: (id: number) => Promise<void>;
|
onRemove: () => void;
|
||||||
}
|
}
|
||||||
export const SegmentDelete = ({
|
export const SegmentDelete = ({
|
||||||
segment,
|
segment,
|
||||||
open,
|
open,
|
||||||
setDeldialogue,
|
onClose,
|
||||||
handleDeleteSegment,
|
onRemove,
|
||||||
}: ISegmentDeleteProps) => {
|
}: ISegmentDeleteProps) => {
|
||||||
const { strategies } = useStrategiesBySegment(segment.id);
|
const { strategies } = useStrategiesBySegment(segment.id);
|
||||||
const canDeleteSegment = strategies?.length === 0;
|
const canDeleteSegment = strategies?.length === 0;
|
||||||
@ -26,15 +26,15 @@ export const SegmentDelete = ({
|
|||||||
<SegmentDeleteConfirm
|
<SegmentDeleteConfirm
|
||||||
segment={segment}
|
segment={segment}
|
||||||
open={open}
|
open={open}
|
||||||
setDeldialogue={setDeldialogue}
|
onClose={onClose}
|
||||||
handleDeleteSegment={handleDeleteSegment}
|
onRemove={onRemove}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<SegmentDeleteUsedSegment
|
<SegmentDeleteUsedSegment
|
||||||
segment={segment}
|
segment={segment}
|
||||||
open={open}
|
open={open}
|
||||||
setDeldialogue={setDeldialogue}
|
onClose={onClose}
|
||||||
strategies={strategies}
|
strategies={strategies}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,15 @@ import { SEGMENT_DIALOG_NAME_ID } from 'utils/testIds';
|
|||||||
interface ISegmentDeleteConfirmProps {
|
interface ISegmentDeleteConfirmProps {
|
||||||
segment: ISegment;
|
segment: ISegment;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
handleDeleteSegment: (id: number) => Promise<void>;
|
onRemove: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SegmentDeleteConfirm = ({
|
export const SegmentDeleteConfirm = ({
|
||||||
segment,
|
segment,
|
||||||
open,
|
open,
|
||||||
setDeldialogue,
|
onClose,
|
||||||
handleDeleteSegment,
|
onRemove,
|
||||||
}: ISegmentDeleteConfirmProps) => {
|
}: ISegmentDeleteConfirmProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const [confirmName, setConfirmName] = useState('');
|
const [confirmName, setConfirmName] = useState('');
|
||||||
@ -25,7 +25,7 @@ export const SegmentDeleteConfirm = ({
|
|||||||
setConfirmName(e.currentTarget.value);
|
setConfirmName(e.currentTarget.value);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setDeldialogue(false);
|
onClose();
|
||||||
setConfirmName('');
|
setConfirmName('');
|
||||||
};
|
};
|
||||||
const formId = 'delete-segment-confirmation-form';
|
const formId = 'delete-segment-confirmation-form';
|
||||||
@ -36,7 +36,7 @@ export const SegmentDeleteConfirm = ({
|
|||||||
primaryButtonText="Delete segment"
|
primaryButtonText="Delete segment"
|
||||||
secondaryButtonText="Cancel"
|
secondaryButtonText="Cancel"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteSegment(segment.id);
|
onRemove();
|
||||||
setConfirmName('');
|
setConfirmName('');
|
||||||
}}
|
}}
|
||||||
disabledPrimaryButton={segment?.name !== confirmName}
|
disabledPrimaryButton={segment?.name !== confirmName}
|
||||||
@ -45,7 +45,7 @@ export const SegmentDeleteConfirm = ({
|
|||||||
>
|
>
|
||||||
<p className={styles.deleteParagraph}>
|
<p className={styles.deleteParagraph}>
|
||||||
In order to delete this segment, please enter the name of the
|
In order to delete this segment, please enter the name of the
|
||||||
segment in the textfield below: <strong>{segment?.name}</strong>
|
segment in the field below: <strong>{segment?.name}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id={formId}>
|
<form id={formId}>
|
||||||
|
@ -10,28 +10,24 @@ import { formatStrategyName } from 'utils/strategyNames';
|
|||||||
interface ISegmentDeleteUsedSegmentProps {
|
interface ISegmentDeleteUsedSegmentProps {
|
||||||
segment: ISegment;
|
segment: ISegment;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
strategies: IFeatureStrategy[] | undefined;
|
strategies: IFeatureStrategy[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SegmentDeleteUsedSegment = ({
|
export const SegmentDeleteUsedSegment = ({
|
||||||
segment,
|
segment,
|
||||||
open,
|
open,
|
||||||
setDeldialogue,
|
onClose,
|
||||||
strategies,
|
strategies,
|
||||||
}: ISegmentDeleteUsedSegmentProps) => {
|
}: ISegmentDeleteUsedSegmentProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setDeldialogue(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
title="You can't delete a segment that's currently in use"
|
title="You can't delete a segment that's currently in use"
|
||||||
open={open}
|
open={open}
|
||||||
primaryButtonText="OK"
|
primaryButtonText="OK"
|
||||||
onClick={handleCancel}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
The following feature toggles are using the{' '}
|
The following feature toggles are using the{' '}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles()(theme => ({
|
||||||
|
empty: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: theme.spacing(6),
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
marginBottom: theme.spacing(2.5),
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
maxWidth: 515,
|
||||||
|
marginBottom: theme.spacing(2.5),
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
paramButton: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { useStyles } from 'component/segments/SegmentEmpty/SegmentEmpty.styles';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const SegmentEmpty = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.empty}>
|
||||||
|
<Typography className={classes.title}>No segments yet!</Typography>
|
||||||
|
<p className={classes.subtitle}>
|
||||||
|
Segment makes it easy for you to define who should be exposed to
|
||||||
|
your feature. The segment is often a collection of constraints
|
||||||
|
and can be reused.
|
||||||
|
</p>
|
||||||
|
<Link to="/segments/create" className={classes.paramButton}>
|
||||||
|
Create your first segment
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,55 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
docs: {
|
|
||||||
marginBottom: '2rem',
|
|
||||||
},
|
|
||||||
empty: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBlock: '5rem',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: theme.fontSizes.mainHeader,
|
|
||||||
marginBottom: '1.25rem',
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
color: theme.palette.grey[600],
|
|
||||||
maxWidth: 515,
|
|
||||||
marginBottom: 20,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
background: '#F6F6FA',
|
|
||||||
borderRadius: '8px',
|
|
||||||
},
|
|
||||||
paramButton: {
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
fontWeight: theme.fontWeight.bold,
|
|
||||||
},
|
|
||||||
cell: {
|
|
||||||
borderBottom: 'none',
|
|
||||||
display: 'table-cell',
|
|
||||||
},
|
|
||||||
firstHeader: {
|
|
||||||
borderTopLeftRadius: '5px',
|
|
||||||
borderBottomLeftRadius: '5px',
|
|
||||||
},
|
|
||||||
lastHeader: {
|
|
||||||
borderTopRightRadius: '5px',
|
|
||||||
borderBottomRightRadius: '5px',
|
|
||||||
},
|
|
||||||
hideSM: {
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hideXS: {
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,179 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import usePagination from 'hooks/usePagination';
|
|
||||||
import { CREATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import PaginateUI from 'component/common/PaginateUI/PaginateUI';
|
|
||||||
import { SegmentListItem } from './SegmentListItem/SegmentListItem';
|
|
||||||
import { ISegment } from 'interfaces/segment';
|
|
||||||
import { useStyles } from './SegmentList.styles';
|
|
||||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
|
||||||
import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi';
|
|
||||||
import useToast from 'hooks/useToast';
|
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
|
||||||
import { SegmentDelete } from '../SegmentDelete/SegmentDelete';
|
|
||||||
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
|
|
||||||
import { NAVIGATE_TO_CREATE_SEGMENT } from 'utils/testIds';
|
|
||||||
|
|
||||||
export const SegmentsList = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { segments = [], refetchSegments } = useSegments();
|
|
||||||
const { deleteSegment } = useSegmentsApi();
|
|
||||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
|
||||||
usePagination(segments, 10);
|
|
||||||
const [currentSegment, setCurrentSegment] = useState<ISegment>();
|
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
|
||||||
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
const onDeleteSegment = async () => {
|
|
||||||
if (!currentSegment?.id) return;
|
|
||||||
try {
|
|
||||||
await deleteSegment(currentSegment?.id);
|
|
||||||
await refetchSegments();
|
|
||||||
setToastData({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Successfully deleted segment',
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
setDelDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSegments = () => {
|
|
||||||
return page.map((segment: ISegment) => {
|
|
||||||
return (
|
|
||||||
<SegmentListItem
|
|
||||||
key={segment.id}
|
|
||||||
id={segment.id}
|
|
||||||
name={segment.name}
|
|
||||||
description={segment.description}
|
|
||||||
createdAt={segment.createdAt}
|
|
||||||
createdBy={segment.createdBy}
|
|
||||||
setCurrentSegment={setCurrentSegment}
|
|
||||||
setDelDialog={setDelDialog}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNoSegments = () => {
|
|
||||||
return (
|
|
||||||
<div className={styles.empty}>
|
|
||||||
<Typography className={styles.title}>
|
|
||||||
No segments yet!
|
|
||||||
</Typography>
|
|
||||||
<p className={styles.subtitle}>
|
|
||||||
Segment makes it easy for you to define who should be
|
|
||||||
exposed to your feature. The segment is often a collection
|
|
||||||
of constraints and can be reused.
|
|
||||||
</p>
|
|
||||||
<Link to="/segments/create" className={styles.paramButton}>
|
|
||||||
Create your first segment
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title="Segments"
|
|
||||||
actions={
|
|
||||||
<PermissionButton
|
|
||||||
onClick={() => navigate('/segments/create')}
|
|
||||||
permission={CREATE_SEGMENT}
|
|
||||||
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
|
|
||||||
>
|
|
||||||
New Segment
|
|
||||||
</PermissionButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.docs}>
|
|
||||||
<SegmentDocsWarning />
|
|
||||||
</div>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow className={styles.tableRow}>
|
|
||||||
<TableCell
|
|
||||||
className={styles.firstHeader}
|
|
||||||
classes={{ root: styles.cell }}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
classes={{ root: styles.cell }}
|
|
||||||
className={styles.hideSM}
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
classes={{ root: styles.cell }}
|
|
||||||
className={styles.hideXS}
|
|
||||||
>
|
|
||||||
Created on
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
classes={{ root: styles.cell }}
|
|
||||||
className={styles.hideXS}
|
|
||||||
>
|
|
||||||
Created By
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
align="right"
|
|
||||||
classes={{ root: styles.cell }}
|
|
||||||
className={styles.lastHeader}
|
|
||||||
>
|
|
||||||
Action
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={segments.length > 0}
|
|
||||||
show={renderSegments()}
|
|
||||||
/>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<PaginateUI
|
|
||||||
pages={pages}
|
|
||||||
pageIndex={pageIndex}
|
|
||||||
setPageIndex={setPageIndex}
|
|
||||||
nextPage={nextPage}
|
|
||||||
prevPage={prevPage}
|
|
||||||
style={{ position: 'static', marginTop: '2rem' }}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={segments.length === 0}
|
|
||||||
show={renderNoSegments()}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(currentSegment)}
|
|
||||||
show={() => (
|
|
||||||
<SegmentDelete
|
|
||||||
segment={currentSegment!}
|
|
||||||
open={delDialog}
|
|
||||||
setDeldialogue={setDelDialog}
|
|
||||||
handleDeleteSegment={onDeleteSegment}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
|
||||||
tableRow: {
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.grey[200],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
leftTableCell: {
|
|
||||||
textAlign: 'left',
|
|
||||||
maxWidth: '300px',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: theme.palette.inactiveIcon,
|
|
||||||
},
|
|
||||||
descriptionCell: {
|
|
||||||
textAlign: 'left',
|
|
||||||
maxWidth: '300px',
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
createdAtCell: {
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
textAlign: 'left',
|
|
||||||
maxWidth: '300px',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,97 +0,0 @@
|
|||||||
import { useStyles } from './SegmentListItem.styles';
|
|
||||||
import { Box, TableCell, TableRow, Typography } from '@mui/material';
|
|
||||||
import { Delete, Edit } from '@mui/icons-material';
|
|
||||||
import {
|
|
||||||
UPDATE_SEGMENT,
|
|
||||||
DELETE_SEGMENT,
|
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
|
||||||
import TimeAgo from 'react-timeago';
|
|
||||||
import { ISegment } from 'interfaces/segment';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { SEGMENT_DELETE_BTN_ID } from 'utils/testIds';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface ISegmentListItemProps {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
createdAt: string;
|
|
||||||
createdBy: string;
|
|
||||||
setCurrentSegment: React.Dispatch<
|
|
||||||
React.SetStateAction<ISegment | undefined>
|
|
||||||
>;
|
|
||||||
setDelDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SegmentListItem = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
createdAt,
|
|
||||||
createdBy,
|
|
||||||
setCurrentSegment,
|
|
||||||
setDelDialog,
|
|
||||||
}: ISegmentListItemProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow className={styles.tableRow}>
|
|
||||||
<TableCell className={styles.leftTableCell}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
{name}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.descriptionCell}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
{description}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.createdAtCell}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
<TimeAgo date={createdAt} live={false} />
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={styles.createdAtCell}>
|
|
||||||
<Typography variant="body2" data-loading>
|
|
||||||
{createdBy}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell align="right">
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<PermissionIconButton
|
|
||||||
data-loading
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/segments/edit/${id}`);
|
|
||||||
}}
|
|
||||||
permission={UPDATE_SEGMENT}
|
|
||||||
tooltipProps={{ title: 'Edit segment' }}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</PermissionIconButton>
|
|
||||||
<PermissionIconButton
|
|
||||||
data-loading
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentSegment({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
createdAt,
|
|
||||||
createdBy,
|
|
||||||
constraints: [],
|
|
||||||
});
|
|
||||||
setDelDialog(true);
|
|
||||||
}}
|
|
||||||
permission={DELETE_SEGMENT}
|
|
||||||
tooltipProps={{ title: 'Remove segment' }}
|
|
||||||
data-testid={`${SEGMENT_DELETE_BTN_ID}_${name}`}
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</PermissionIconButton>
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
199
frontend/src/component/segments/SegmentTable/SegmentTable.tsx
Normal file
199
frontend/src/component/segments/SegmentTable/SegmentTable.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import {
|
||||||
|
TableSearch,
|
||||||
|
SortableTableHeader,
|
||||||
|
TableCell,
|
||||||
|
TablePlaceholder,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
} from 'component/common/Table';
|
||||||
|
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||||
|
import { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { useMediaQuery, Box } from '@mui/material';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
|
import { useMemo, useEffect, useState } from 'react';
|
||||||
|
import { SegmentEmpty } from 'component/segments/SegmentEmpty/SegmentEmpty';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { DonutLarge } from '@mui/icons-material';
|
||||||
|
import { SegmentActionCell } from 'component/segments/SegmentActionCell/SegmentActionCell';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
export const SegmentTable = () => {
|
||||||
|
const { segments, loading } = useSegments();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [initialState] = useState({
|
||||||
|
sortBy: [{ id: 'createdAt', desc: false }],
|
||||||
|
hiddenColumns: ['description'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return (
|
||||||
|
segments ??
|
||||||
|
Array(5).fill({
|
||||||
|
name: 'Segment name',
|
||||||
|
description: 'Segment descripton',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
createdBy: 'user',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [segments]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
setGlobalFilter,
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
initialState,
|
||||||
|
columns: COLUMNS as any,
|
||||||
|
data: data as any,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
defaultColumn: {
|
||||||
|
Cell: HighlightCell,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSmallScreen) {
|
||||||
|
setHiddenColumns(['description', 'createdAt', 'createdBy']);
|
||||||
|
} else {
|
||||||
|
setHiddenColumns(['description']);
|
||||||
|
}
|
||||||
|
}, [setHiddenColumns, isSmallScreen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title="Segments"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<TableSearch
|
||||||
|
initialValue={globalFilter}
|
||||||
|
onChange={setGlobalFilter}
|
||||||
|
/>
|
||||||
|
<PageHeader.Divider />
|
||||||
|
<CreateSegmentButton />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isLoading={loading}
|
||||||
|
>
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<SegmentDocsWarning />
|
||||||
|
</Box>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!loading && data.length === 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
<SegmentEmpty />
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={() => (
|
||||||
|
<>
|
||||||
|
<SearchHighlightProvider value={globalFilter}>
|
||||||
|
<Table {...getTableProps()} rowHeight="standard">
|
||||||
|
<SortableTableHeader
|
||||||
|
headerGroups={headerGroups as any}
|
||||||
|
/>
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
{...row.getRowProps()}
|
||||||
|
>
|
||||||
|
{row.cells.map(cell => (
|
||||||
|
<TableCell
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
rows.length === 0 && globalFilter?.length > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No segments found matching “
|
||||||
|
{globalFilter}”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
id: 'Icon',
|
||||||
|
width: '1%',
|
||||||
|
canSort: false,
|
||||||
|
Cell: () => <IconCell icon={<DonutLarge color="disabled" />} />,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: '80%',
|
||||||
|
Cell: ({ value, row: { original } }: any) => (
|
||||||
|
<HighlightCell value={value} subtitle={original.description} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Created at',
|
||||||
|
accessor: 'createdAt',
|
||||||
|
minWidth: 150,
|
||||||
|
Cell: DateCell,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Created by',
|
||||||
|
accessor: 'createdBy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
width: '1%',
|
||||||
|
canSort: false,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<SegmentActionCell segment={original} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
},
|
||||||
|
];
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
exports[`renders an empty list correctly 1`] = `
|
exports[`renders an empty list correctly 1`] = `
|
||||||
[
|
[
|
||||||
<div>
|
<div
|
||||||
|
aria-busy={true}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 tss-15wj2kz-container mui-177gdp-MuiPaper-root"
|
className="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 tss-15wj2kz-container mui-177gdp-MuiPaper-root"
|
||||||
>
|
>
|
||||||
@ -109,7 +112,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-54jt3w-bodyContainer"
|
className="tss-54jt3w-bodyContainer"
|
||||||
>
|
>
|
||||||
<table
|
<table
|
||||||
className="MuiTable-root mui-133vm37-MuiTable-root"
|
className="MuiTable-root tss-z7cn64-table mui-133vm37-MuiTable-root"
|
||||||
role="table"
|
role="table"
|
||||||
>
|
>
|
||||||
<thead
|
<thead
|
||||||
@ -268,7 +271,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-1sfnvgj-container"
|
className="tss-1sfnvgj-container"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="tss-7fbt0h-title"
|
className="tss-lcjmxf-title"
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@ -432,7 +435,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-1sfnvgj-container"
|
className="tss-1sfnvgj-container"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="tss-7fbt0h-title"
|
className="tss-lcjmxf-title"
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@ -596,7 +599,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-1sfnvgj-container"
|
className="tss-1sfnvgj-container"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="tss-7fbt0h-title"
|
className="tss-lcjmxf-title"
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@ -760,7 +763,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-1sfnvgj-container"
|
className="tss-1sfnvgj-container"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="tss-7fbt0h-title"
|
className="tss-lcjmxf-title"
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@ -924,7 +927,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
className="tss-1sfnvgj-container"
|
className="tss-1sfnvgj-container"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="tss-7fbt0h-title"
|
className="tss-lcjmxf-title"
|
||||||
data-loading={true}
|
data-loading={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
@ -18,7 +18,10 @@ export const useSegments = (strategyId?: string): IUseSegmentsOutput => {
|
|||||||
|
|
||||||
const { data, error, mutate } = useSWR(
|
const { data, error, mutate } = useSWR(
|
||||||
[strategyId, uiConfig.flags],
|
[strategyId, uiConfig.flags],
|
||||||
fetchSegments
|
fetchSegments,
|
||||||
|
{
|
||||||
|
refreshInterval: 15 * 1000,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const refetchSegments = useCallback(() => {
|
const refetchSegments = useCallback(() => {
|
||||||
|
@ -45,6 +45,7 @@ export default createTheme({
|
|||||||
borderRadiusLarge: '12px',
|
borderRadiusLarge: '12px',
|
||||||
borderRadiusExtraLarge: '20px',
|
borderRadiusExtraLarge: '20px',
|
||||||
tableRowHeight: 64,
|
tableRowHeight: 64,
|
||||||
|
tableRowHeightCompact: 56,
|
||||||
tableRowHeightDense: 48,
|
tableRowHeightDense: 48,
|
||||||
},
|
},
|
||||||
palette: {
|
palette: {
|
||||||
|
@ -109,6 +109,7 @@ declare module '@mui/system/createTheme/shape' {
|
|||||||
borderRadiusLarge: string;
|
borderRadiusLarge: string;
|
||||||
borderRadiusExtraLarge: string;
|
borderRadiusExtraLarge: string;
|
||||||
tableRowHeight: number;
|
tableRowHeight: number;
|
||||||
|
tableRowHeightCompact: number;
|
||||||
tableRowHeightDense: number;
|
tableRowHeightDense: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user