mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +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}
|
||||
setSort={setSort}
|
||||
>
|
||||
Created on
|
||||
Created
|
||||
</TableCellSortable>
|
||||
<TableCell
|
||||
align="center"
|
||||
|
@ -49,7 +49,7 @@ export const PageContent: FC<IPageContentProps> = ({
|
||||
const paperProps = disableBorder ? { elevation: 0 } : {};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={ref} aria-busy={isLoading} aria-live="polite">
|
||||
<Paper
|
||||
{...rest}
|
||||
{...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) => {
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
@ -17,5 +17,5 @@ export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
||||
: formatDateYMD(parseISO(value), locationSettings.locale)
|
||||
: 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 { Box } from '@mui/material';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
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 {
|
||||
value?: string | null;
|
||||
children?: string | null;
|
||||
value: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export const HighlightCell: VFC<IHighlightCellProps> = ({
|
||||
value,
|
||||
children,
|
||||
subtitle,
|
||||
}) => {
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const text = children ?? value;
|
||||
if (!text) {
|
||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
||||
}
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 1.5, px: 2 }}>
|
||||
<span data-loading role="tooltip">
|
||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||
<Box className={classes.container}>
|
||||
<span
|
||||
className={classes.title}
|
||||
style={{
|
||||
WebkitLineClamp: Boolean(subtitle) ? 1 : 2,
|
||||
lineClamp: Boolean(subtitle) ? 1 : 2,
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<Highlighter search={searchQuery}>{value}</Highlighter>
|
||||
</span>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(subtitle)}
|
||||
show={() => (
|
||||
<Typography
|
||||
component="span"
|
||||
className={classes.subtitle}
|
||||
data-loading
|
||||
>
|
||||
<Highlighter search={searchQuery}>
|
||||
{subtitle}
|
||||
</Highlighter>
|
||||
</Typography>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -8,13 +8,11 @@ interface IIconCellProps {
|
||||
export const IconCell = ({ icon }: IIconCellProps) => {
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{
|
||||
pl: 2,
|
||||
pr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 60,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
|
@ -28,6 +28,7 @@ export const useStyles = makeStyles()(theme => ({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
},
|
||||
description: {
|
||||
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 { Box } from '@mui/material';
|
||||
import { useStyles } from './TextCell.styles';
|
||||
|
||||
interface ITextCellProps {
|
||||
value?: string | null;
|
||||
lineClamp?: number;
|
||||
}
|
||||
|
||||
export const TextCell: FC<ITextCellProps> = ({ value, children }) => {
|
||||
const text = children ?? value;
|
||||
if (!text) {
|
||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
||||
}
|
||||
export const TextCell: FC<ITextCellProps> = ({
|
||||
value,
|
||||
children,
|
||||
lineClamp,
|
||||
}) => {
|
||||
const { classes } = useStyles({ lineClamp });
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 1.5, px: 2 }}>
|
||||
<span data-loading role="tooltip">
|
||||
{text}
|
||||
</span>
|
||||
<Box className={classes.wrapper}>
|
||||
<span data-loading>{children ?? value}</span>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
export { TableSearch } from './TableSearch/TableSearch';
|
||||
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 { TablePlaceholder } from './TablePlaceholder/TablePlaceholder';
|
||||
|
@ -112,8 +112,8 @@ const COLUMNS = [
|
||||
accessor: 'name',
|
||||
width: '100%',
|
||||
canSort: false,
|
||||
Cell: (props: any) => (
|
||||
<EnvironmentNameCell environment={props.row.original} />
|
||||
Cell: ({ row: { original } }: any) => (
|
||||
<EnvironmentNameCell environment={original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -121,8 +121,8 @@ const COLUMNS = [
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
canSort: false,
|
||||
Cell: (props: any) => (
|
||||
<EnvironmentActionCell environment={props.row.original} />
|
||||
Cell: ({ row: { original } }: any) => (
|
||||
<EnvironmentActionCell environment={original} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -68,7 +68,7 @@ const columns = [
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
{
|
||||
Header: 'Created on',
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
@ -236,7 +236,7 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={globalFilter}>
|
||||
<Table {...getTableProps()}>
|
||||
<Table {...getTableProps()} rowHeight={rowHeight}>
|
||||
{/* @ts-expect-error -- fix in react-table v8 */}
|
||||
<SortableTableHeader headerGroups={headerGroups} flex />
|
||||
<TableBody
|
||||
|
@ -36,7 +36,6 @@ export const useStyles = makeStyles()(theme => ({
|
||||
alignItems: 'center',
|
||||
},
|
||||
row: {
|
||||
height: theme.shape.tableRowHeight,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
|
@ -49,9 +49,9 @@ import { SplashPage } from 'component/splash/SplashPage/SplashPage';
|
||||
import { CreateUnleashContextPage } from 'component/context/CreateUnleashContext/CreateUnleashContextPage';
|
||||
import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
|
||||
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
||||
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
|
||||
import { IRoute } from 'interfaces/route';
|
||||
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
||||
import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
|
||||
import RedirectAdminInvoices from 'component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices';
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
@ -348,7 +348,7 @@ export const routes: IRoute[] = [
|
||||
{
|
||||
path: '/segments',
|
||||
title: 'Segments',
|
||||
component: SegmentsList,
|
||||
component: SegmentTable,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
menu: { mobile: true, advanced: true },
|
||||
|
@ -203,7 +203,7 @@ export const ProjectFeatureToggles = ({
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
{
|
||||
Header: 'Created on',
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
|
@ -142,7 +142,7 @@ export const ProjectAccessTable: VFC<IProjectAccessTableProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Table {...getTableProps()}>
|
||||
<Table {...getTableProps()} rowHeight="compact">
|
||||
{/* @ts-expect-error -- react-table */}
|
||||
<SortableTableHeader headerGroups={headerGroups} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
|
@ -2,6 +2,8 @@ import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
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 {
|
||||
segment: ISegment;
|
||||
open: boolean;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteSegment: (id: number) => Promise<void>;
|
||||
onClose: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
export const SegmentDelete = ({
|
||||
segment,
|
||||
open,
|
||||
setDeldialogue,
|
||||
handleDeleteSegment,
|
||||
onClose,
|
||||
onRemove,
|
||||
}: ISegmentDeleteProps) => {
|
||||
const { strategies } = useStrategiesBySegment(segment.id);
|
||||
const canDeleteSegment = strategies?.length === 0;
|
||||
@ -26,15 +26,15 @@ export const SegmentDelete = ({
|
||||
<SegmentDeleteConfirm
|
||||
segment={segment}
|
||||
open={open}
|
||||
setDeldialogue={setDeldialogue}
|
||||
handleDeleteSegment={handleDeleteSegment}
|
||||
onClose={onClose}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<SegmentDeleteUsedSegment
|
||||
segment={segment}
|
||||
open={open}
|
||||
setDeldialogue={setDeldialogue}
|
||||
onClose={onClose}
|
||||
strategies={strategies}
|
||||
/>
|
||||
}
|
||||
|
@ -8,15 +8,15 @@ import { SEGMENT_DIALOG_NAME_ID } from 'utils/testIds';
|
||||
interface ISegmentDeleteConfirmProps {
|
||||
segment: ISegment;
|
||||
open: boolean;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteSegment: (id: number) => Promise<void>;
|
||||
onClose: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
export const SegmentDeleteConfirm = ({
|
||||
segment,
|
||||
open,
|
||||
setDeldialogue,
|
||||
handleDeleteSegment,
|
||||
onClose,
|
||||
onRemove,
|
||||
}: ISegmentDeleteConfirmProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
@ -25,7 +25,7 @@ export const SegmentDeleteConfirm = ({
|
||||
setConfirmName(e.currentTarget.value);
|
||||
|
||||
const handleCancel = () => {
|
||||
setDeldialogue(false);
|
||||
onClose();
|
||||
setConfirmName('');
|
||||
};
|
||||
const formId = 'delete-segment-confirmation-form';
|
||||
@ -36,7 +36,7 @@ export const SegmentDeleteConfirm = ({
|
||||
primaryButtonText="Delete segment"
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={() => {
|
||||
handleDeleteSegment(segment.id);
|
||||
onRemove();
|
||||
setConfirmName('');
|
||||
}}
|
||||
disabledPrimaryButton={segment?.name !== confirmName}
|
||||
@ -45,7 +45,7 @@ export const SegmentDeleteConfirm = ({
|
||||
>
|
||||
<p className={styles.deleteParagraph}>
|
||||
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>
|
||||
|
||||
<form id={formId}>
|
||||
|
@ -10,28 +10,24 @@ import { formatStrategyName } from 'utils/strategyNames';
|
||||
interface ISegmentDeleteUsedSegmentProps {
|
||||
segment: ISegment;
|
||||
open: boolean;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onClose: () => void;
|
||||
strategies: IFeatureStrategy[] | undefined;
|
||||
}
|
||||
|
||||
export const SegmentDeleteUsedSegment = ({
|
||||
segment,
|
||||
open,
|
||||
setDeldialogue,
|
||||
onClose,
|
||||
strategies,
|
||||
}: ISegmentDeleteUsedSegmentProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const handleCancel = () => {
|
||||
setDeldialogue(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
title="You can't delete a segment that's currently in use"
|
||||
open={open}
|
||||
primaryButtonText="OK"
|
||||
onClick={handleCancel}
|
||||
onClick={onClose}
|
||||
>
|
||||
<p>
|
||||
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`] = `
|
||||
[
|
||||
<div>
|
||||
<div
|
||||
aria-busy={true}
|
||||
aria-live="polite"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<table
|
||||
className="MuiTable-root mui-133vm37-MuiTable-root"
|
||||
className="MuiTable-root tss-z7cn64-table mui-133vm37-MuiTable-root"
|
||||
role="table"
|
||||
>
|
||||
<thead
|
||||
@ -268,7 +271,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
className="tss-1sfnvgj-container"
|
||||
>
|
||||
<span
|
||||
className="tss-7fbt0h-title"
|
||||
className="tss-lcjmxf-title"
|
||||
data-loading={true}
|
||||
style={
|
||||
{
|
||||
@ -432,7 +435,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
className="tss-1sfnvgj-container"
|
||||
>
|
||||
<span
|
||||
className="tss-7fbt0h-title"
|
||||
className="tss-lcjmxf-title"
|
||||
data-loading={true}
|
||||
style={
|
||||
{
|
||||
@ -596,7 +599,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
className="tss-1sfnvgj-container"
|
||||
>
|
||||
<span
|
||||
className="tss-7fbt0h-title"
|
||||
className="tss-lcjmxf-title"
|
||||
data-loading={true}
|
||||
style={
|
||||
{
|
||||
@ -760,7 +763,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
className="tss-1sfnvgj-container"
|
||||
>
|
||||
<span
|
||||
className="tss-7fbt0h-title"
|
||||
className="tss-lcjmxf-title"
|
||||
data-loading={true}
|
||||
style={
|
||||
{
|
||||
@ -924,7 +927,7 @@ exports[`renders an empty list correctly 1`] = `
|
||||
className="tss-1sfnvgj-container"
|
||||
>
|
||||
<span
|
||||
className="tss-7fbt0h-title"
|
||||
className="tss-lcjmxf-title"
|
||||
data-loading={true}
|
||||
style={
|
||||
{
|
||||
|
@ -18,7 +18,10 @@ export const useSegments = (strategyId?: string): IUseSegmentsOutput => {
|
||||
|
||||
const { data, error, mutate } = useSWR(
|
||||
[strategyId, uiConfig.flags],
|
||||
fetchSegments
|
||||
fetchSegments,
|
||||
{
|
||||
refreshInterval: 15 * 1000,
|
||||
}
|
||||
);
|
||||
|
||||
const refetchSegments = useCallback(() => {
|
||||
|
@ -45,6 +45,7 @@ export default createTheme({
|
||||
borderRadiusLarge: '12px',
|
||||
borderRadiusExtraLarge: '20px',
|
||||
tableRowHeight: 64,
|
||||
tableRowHeightCompact: 56,
|
||||
tableRowHeightDense: 48,
|
||||
},
|
||||
palette: {
|
||||
|
@ -109,6 +109,7 @@ declare module '@mui/system/createTheme/shape' {
|
||||
borderRadiusLarge: string;
|
||||
borderRadiusExtraLarge: string;
|
||||
tableRowHeight: number;
|
||||
tableRowHeightCompact: number;
|
||||
tableRowHeightDense: number;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user