From 504a4af274c6c852e3b6d59e24674be943066feb Mon Sep 17 00:00:00 2001 From: olav Date: Fri, 27 May 2022 08:57:30 +0200 Subject: [PATCH] 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 --- .../ProjectRoleListItem.styles.ts | 30 --- .../ProjectRoleListItem.tsx | 81 ------- .../admin/users/UsersList/UsersList.tsx | 2 +- .../common/PageContent/PageContent.tsx | 2 +- .../common/Table/Table/Table.styles.ts | 17 ++ .../component/common/Table/Table/Table.tsx | 16 ++ .../Table/cells/ActionCell/ActionCell.tsx | 5 +- .../common/Table/cells/DateCell/DateCell.tsx | 2 +- .../HighlightCell/HighlightCell.styles.ts | 29 +++ .../cells/HighlightCell/HighlightCell.tsx | 43 ++-- .../common/Table/cells/IconCell/IconCell.tsx | 2 - .../Table/cells/LinkCell/LinkCell.styles.ts | 1 + .../Table/cells/TextCell/TextCell.styles.ts | 13 ++ .../common/Table/cells/TextCell/TextCell.tsx | 19 +- frontend/src/component/common/Table/index.ts | 3 +- .../EnvironmentTable/EnvironmentTable.tsx | 8 +- .../FeatureToggleListTable.tsx | 4 +- .../feature/FeatureToggleList/styles.ts | 1 - frontend/src/component/menu/routes.ts | 4 +- .../ProjectFeatureToggles.tsx | 2 +- .../ProjectAccessTable/ProjectAccessTable.tsx | 2 +- .../ProjectRoleCell.styles.tsx | 4 +- .../CreateSegmentButton.tsx | 18 ++ .../EditSegmentButton/EditSegmentButton.tsx | 23 ++ .../RemoveSegmentButton.tsx | 62 ++++++ .../SegmentActionCell/SegmentActionCell.tsx | 17 ++ .../segments/SegmentDelete/SegmentDelete.tsx | 14 +- .../SegmentDeleteConfirm.tsx | 14 +- .../SegmentDeleteUsedSegment.tsx | 10 +- .../SegmentEmpty/SegmentEmpty.styles.ts | 29 +++ .../segments/SegmentEmpty/SegmentEmpty.tsx | 21 ++ .../SegmentList/SegmentList.styles.ts | 55 ----- .../segments/SegmentList/SegmentList.tsx | 179 ---------------- .../SegmentListItem/SegmentListItem.styles.ts | 30 --- .../SegmentListItem/SegmentListItem.tsx | 97 --------- .../segments/SegmentTable/SegmentTable.tsx | 199 ++++++++++++++++++ .../__snapshots__/TagTypeList.test.tsx.snap | 17 +- .../api/getters/useSegments/useSegments.ts | 5 +- frontend/src/themes/theme.ts | 1 + frontend/src/themes/themeTypes.ts | 1 + 40 files changed, 538 insertions(+), 544 deletions(-) delete mode 100644 frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.styles.ts delete mode 100644 frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.tsx create mode 100644 frontend/src/component/common/Table/Table/Table.styles.ts create mode 100644 frontend/src/component/common/Table/Table/Table.tsx create mode 100644 frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.styles.ts create mode 100644 frontend/src/component/common/Table/cells/TextCell/TextCell.styles.ts create mode 100644 frontend/src/component/segments/CreateSegmentButton/CreateSegmentButton.tsx create mode 100644 frontend/src/component/segments/EditSegmentButton/EditSegmentButton.tsx create mode 100644 frontend/src/component/segments/RemoveSegmentButton/RemoveSegmentButton.tsx create mode 100644 frontend/src/component/segments/SegmentActionCell/SegmentActionCell.tsx create mode 100644 frontend/src/component/segments/SegmentEmpty/SegmentEmpty.styles.ts create mode 100644 frontend/src/component/segments/SegmentEmpty/SegmentEmpty.tsx delete mode 100644 frontend/src/component/segments/SegmentList/SegmentList.styles.ts delete mode 100644 frontend/src/component/segments/SegmentList/SegmentList.tsx delete mode 100644 frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.styles.ts delete mode 100644 frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx create mode 100644 frontend/src/component/segments/SegmentTable/SegmentTable.tsx diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.styles.ts b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.styles.ts deleted file mode 100644 index f56f1f4782..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.styles.ts +++ /dev/null @@ -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', - }, - }, -})); diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.tsx deleted file mode 100644 index 657238ee1e..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleListItem/ProjectRoleListItem.tsx +++ /dev/null @@ -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>; - setDelDialog: React.Dispatch>; -} - -const BUILTIN_ROLE_TYPE = 'project'; - -const RoleListItem = ({ - id, - name, - type, - description, - setCurrentRole, - setDelDialog, -}: IRoleListItemProps) => { - const navigate = useNavigate(); - const { classes: styles } = useStyles(); - - return ( - <> - - - - - - - {name} - - - - - {description} - - - - - { - navigate(`/admin/roles/${id}/edit`); - }} - permission={ADMIN} - tooltipProps={{ title: 'Edit role' }} - > - - - { - // @ts-expect-error - setCurrentRole({ id, name, description }); - setDelDialog(true); - }} - permission={ADMIN} - tooltipProps={{ title: 'Remove role' }} - > - - - - - - ); -}; - -export default RoleListItem; diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index b71922565e..e88a6d0054 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -179,7 +179,7 @@ const UsersList = ({ search }: IUsersListProps) => { sort={sort} setSort={setSort} > - Created on + Created = ({ const paperProps = disableBorder ? { elevation: 0 } : {}; return ( -
+
()((theme, { rowHeight }) => ({ + table: { + '& tbody tr': { + height: + { + auto: 'auto', + standard: theme.shape.tableRowHeight, + compact: theme.shape.tableRowHeightCompact, + dense: theme.shape.tableRowHeightDense, + }[rowHeight] ?? rowHeight, + }, + }, +})); diff --git a/frontend/src/component/common/Table/Table/Table.tsx b/frontend/src/component/common/Table/Table/Table.tsx new file mode 100644 index 0000000000..86ae22607b --- /dev/null +++ b/frontend/src/component/common/Table/Table/Table.tsx @@ -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 ( + + ); +}; diff --git a/frontend/src/component/common/Table/cells/ActionCell/ActionCell.tsx b/frontend/src/component/common/Table/cells/ActionCell/ActionCell.tsx index 0667b0292a..ff89762d43 100644 --- a/frontend/src/component/common/Table/cells/ActionCell/ActionCell.tsx +++ b/frontend/src/component/common/Table/cells/ActionCell/ActionCell.tsx @@ -7,10 +7,7 @@ interface IContextActionsCellProps { export const ActionCell = ({ children }: IContextActionsCellProps) => { return ( - + {children} ); diff --git a/frontend/src/component/common/Table/cells/DateCell/DateCell.tsx b/frontend/src/component/common/Table/cells/DateCell/DateCell.tsx index 121a4b6147..388eed8f41 100644 --- a/frontend/src/component/common/Table/cells/DateCell/DateCell.tsx +++ b/frontend/src/component/common/Table/cells/DateCell/DateCell.tsx @@ -17,5 +17,5 @@ export const DateCell: VFC = ({ value }) => { : formatDateYMD(parseISO(value), locationSettings.locale) : undefined; - return {date}; + return {date}; }; diff --git a/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.styles.ts b/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.styles.ts new file mode 100644 index 0000000000..3b47620556 --- /dev/null +++ b/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.styles.ts @@ -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', + }, +})); diff --git a/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.tsx b/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.tsx index 0292fa1034..9690312ee0 100644 --- a/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.tsx +++ b/frontend/src/component/common/Table/cells/HighlightCell/HighlightCell.tsx @@ -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 = ({ value, - children, + subtitle, }) => { const { searchQuery } = useSearchHighlightContext(); - - const text = children ?? value; - if (!text) { - return ; - } + const { classes } = useStyles(); return ( - - - {text} + + + {value} + ( + + + {subtitle} + + + )} + /> ); }; diff --git a/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx b/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx index 39f2675f5f..5c44952b07 100644 --- a/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx +++ b/frontend/src/component/common/Table/cells/IconCell/IconCell.tsx @@ -8,13 +8,11 @@ interface IIconCellProps { export const IconCell = ({ icon }: IIconCellProps) => { return ( {icon} diff --git a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts index 2497c2f55c..4e900c3329 100644 --- a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts +++ b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts @@ -28,6 +28,7 @@ export const useStyles = makeStyles()(theme => ({ overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', + WebkitBoxOrient: 'vertical', }, description: { color: theme.palette.text.secondary, diff --git a/frontend/src/component/common/Table/cells/TextCell/TextCell.styles.ts b/frontend/src/component/common/Table/cells/TextCell/TextCell.styles.ts new file mode 100644 index 0000000000..b8c98cfbd1 --- /dev/null +++ b/frontend/src/component/common/Table/cells/TextCell/TextCell.styles.ts @@ -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', + }, + }) +); diff --git a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx index bfeb186d58..bf5944d941 100644 --- a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx +++ b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx @@ -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 = ({ value, children }) => { - const text = children ?? value; - if (!text) { - return ; - } +export const TextCell: FC = ({ + value, + children, + lineClamp, +}) => { + const { classes } = useStyles({ lineClamp }); return ( - - - {text} - + + {children ?? value} ); }; diff --git a/frontend/src/component/common/Table/index.ts b/frontend/src/component/common/Table/index.ts index 08f82dbcc7..6f70e71d46 100644 --- a/frontend/src/component/common/Table/index.ts +++ b/frontend/src/component/common/Table/index.ts @@ -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'; diff --git a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx index ddcca9cfa6..ed4963d40f 100644 --- a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx +++ b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx @@ -112,8 +112,8 @@ const COLUMNS = [ accessor: 'name', width: '100%', canSort: false, - Cell: (props: any) => ( - + Cell: ({ row: { original } }: any) => ( + ), }, { @@ -121,8 +121,8 @@ const COLUMNS = [ id: 'Actions', align: 'center', canSort: false, - Cell: (props: any) => ( - + Cell: ({ row: { original } }: any) => ( + ), }, ]; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 54207fe238..59d28b1384 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -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 = () => { } > - +
{/* @ts-expect-error -- fix in react-table v8 */} ({ alignItems: 'center', }, row: { - height: theme.shape.tableRowHeight, position: 'absolute', width: '100%', }, diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 673359fa53..e6710aa056 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -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 }, diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 201be258b0..1e17ce6ecd 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -203,7 +203,7 @@ export const ProjectFeatureToggles = ({ sortType: 'alphanumeric', }, { - Header: 'Created on', + Header: 'Created', accessor: 'createdAt', Cell: DateCell, sortType: 'date', diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx index 1616c244ed..0ff9c13433 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx @@ -142,7 +142,7 @@ export const ProjectAccessTable: VFC = ({ ); return ( -
+
{/* @ts-expect-error -- react-table */} diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectRoleCell/ProjectRoleCell.styles.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectRoleCell/ProjectRoleCell.styles.tsx index 2418e884ca..9b5cb9c454 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectRoleCell/ProjectRoleCell.styles.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectRoleCell/ProjectRoleCell.styles.tsx @@ -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', }, })); diff --git a/frontend/src/component/segments/CreateSegmentButton/CreateSegmentButton.tsx b/frontend/src/component/segments/CreateSegmentButton/CreateSegmentButton.tsx new file mode 100644 index 0000000000..192a44fda8 --- /dev/null +++ b/frontend/src/component/segments/CreateSegmentButton/CreateSegmentButton.tsx @@ -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 ( + navigate('/segments/create')} + permission={CREATE_SEGMENT} + data-testid={NAVIGATE_TO_CREATE_SEGMENT} + > + New segment + + ); +}; diff --git a/frontend/src/component/segments/EditSegmentButton/EditSegmentButton.tsx b/frontend/src/component/segments/EditSegmentButton/EditSegmentButton.tsx new file mode 100644 index 0000000000..98a2cb822e --- /dev/null +++ b/frontend/src/component/segments/EditSegmentButton/EditSegmentButton.tsx @@ -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 ( + navigate(`/segments/edit/${segment.id}`)} + permission={UPDATE_SEGMENT} + tooltipProps={{ title: 'Edit segment' }} + > + + + ); +}; diff --git a/frontend/src/component/segments/RemoveSegmentButton/RemoveSegmentButton.tsx b/frontend/src/component/segments/RemoveSegmentButton/RemoveSegmentButton.tsx new file mode 100644 index 0000000000..5c363decea --- /dev/null +++ b/frontend/src/component/segments/RemoveSegmentButton/RemoveSegmentButton.tsx @@ -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 ( + <> + toggleModal(true)} + permission={DELETE_SEGMENT} + tooltipProps={{ title: 'Remove segment' }} + data-testid={`${SEGMENT_DELETE_BTN_ID}_${segment.name}`} + > + + + ( + toggleModal(false)} + onRemove={onRemove} + /> + )} + /> + + ); +}; diff --git a/frontend/src/component/segments/SegmentActionCell/SegmentActionCell.tsx b/frontend/src/component/segments/SegmentActionCell/SegmentActionCell.tsx new file mode 100644 index 0000000000..a6e09f83f1 --- /dev/null +++ b/frontend/src/component/segments/SegmentActionCell/SegmentActionCell.tsx @@ -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 ( + + + + + ); +}; diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx index 63c936adaa..c12cb47f2f 100644 --- a/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx +++ b/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx @@ -8,14 +8,14 @@ import { SegmentDeleteUsedSegment } from './SegmentDeleteUsedSegment/SegmentDele interface ISegmentDeleteProps { segment: ISegment; open: boolean; - setDeldialogue: React.Dispatch>; - handleDeleteSegment: (id: number) => Promise; + 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 = ({ } elseShow={ } diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDeleteConfirm/SegmentDeleteConfirm.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDeleteConfirm/SegmentDeleteConfirm.tsx index d05d54fcc1..43d1fafa93 100644 --- a/frontend/src/component/segments/SegmentDelete/SegmentDeleteConfirm/SegmentDeleteConfirm.tsx +++ b/frontend/src/component/segments/SegmentDelete/SegmentDeleteConfirm/SegmentDeleteConfirm.tsx @@ -8,15 +8,15 @@ import { SEGMENT_DIALOG_NAME_ID } from 'utils/testIds'; interface ISegmentDeleteConfirmProps { segment: ISegment; open: boolean; - setDeldialogue: React.Dispatch>; - handleDeleteSegment: (id: number) => Promise; + 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 = ({ >

In order to delete this segment, please enter the name of the - segment in the textfield below: {segment?.name} + segment in the field below: {segment?.name}

diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx index 354ec5a298..df5de5fd4d 100644 --- a/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx +++ b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx @@ -10,28 +10,24 @@ import { formatStrategyName } from 'utils/strategyNames'; interface ISegmentDeleteUsedSegmentProps { segment: ISegment; open: boolean; - setDeldialogue: React.Dispatch>; + onClose: () => void; strategies: IFeatureStrategy[] | undefined; } export const SegmentDeleteUsedSegment = ({ segment, open, - setDeldialogue, + onClose, strategies, }: ISegmentDeleteUsedSegmentProps) => { const { classes: styles } = useStyles(); - const handleCancel = () => { - setDeldialogue(false); - }; - return (

The following feature toggles are using the{' '} diff --git a/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.styles.ts b/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.styles.ts new file mode 100644 index 0000000000..2cf88bedda --- /dev/null +++ b/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.styles.ts @@ -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, + }, +})); diff --git a/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.tsx b/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.tsx new file mode 100644 index 0000000000..1b3b696f81 --- /dev/null +++ b/frontend/src/component/segments/SegmentEmpty/SegmentEmpty.tsx @@ -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 ( +

+ No segments yet! +

+ 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. +

+ + Create your first segment + +
+ ); +}; diff --git a/frontend/src/component/segments/SegmentList/SegmentList.styles.ts b/frontend/src/component/segments/SegmentList/SegmentList.styles.ts deleted file mode 100644 index c8c50e2644..0000000000 --- a/frontend/src/component/segments/SegmentList/SegmentList.styles.ts +++ /dev/null @@ -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', - }, - }, -})); diff --git a/frontend/src/component/segments/SegmentList/SegmentList.tsx b/frontend/src/component/segments/SegmentList/SegmentList.tsx deleted file mode 100644 index 819633c752..0000000000 --- a/frontend/src/component/segments/SegmentList/SegmentList.tsx +++ /dev/null @@ -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(); - 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 ( - - ); - }); - }; - - const renderNoSegments = () => { - return ( -
- - No segments yet! - -

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

- - Create your first segment - -
- ); - }; - - return ( - navigate('/segments/create')} - permission={CREATE_SEGMENT} - data-testid={NAVIGATE_TO_CREATE_SEGMENT} - > - New Segment - - } - /> - } - > -
- -
-
- - - - Name - - - Description - - - Created on - - - Created By - - - Action - - - - - 0} - show={renderSegments()} - /> - -
- - - ( - - )} - /> - - ); -}; diff --git a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.styles.ts b/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.styles.ts deleted file mode 100644 index e890db901e..0000000000 --- a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.styles.ts +++ /dev/null @@ -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', - }, -})); diff --git a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx b/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx deleted file mode 100644 index 62d89bcd5a..0000000000 --- a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx +++ /dev/null @@ -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 - >; - setDelDialog: React.Dispatch>; -} - -export const SegmentListItem = ({ - id, - name, - description, - createdAt, - createdBy, - setCurrentSegment, - setDelDialog, -}: ISegmentListItemProps) => { - const { classes: styles } = useStyles(); - const navigate = useNavigate(); - - return ( - - - - {name} - - - - - {description} - - - - - - - - - - {createdBy} - - - - - - { - navigate(`/segments/edit/${id}`); - }} - permission={UPDATE_SEGMENT} - tooltipProps={{ title: 'Edit segment' }} - > - - - { - setCurrentSegment({ - id, - name, - description, - createdAt, - createdBy, - constraints: [], - }); - setDelDialog(true); - }} - permission={DELETE_SEGMENT} - tooltipProps={{ title: 'Remove segment' }} - data-testid={`${SEGMENT_DELETE_BTN_ID}_${name}`} - > - - - - - - ); -}; diff --git a/frontend/src/component/segments/SegmentTable/SegmentTable.tsx b/frontend/src/component/segments/SegmentTable/SegmentTable.tsx new file mode 100644 index 0000000000..7f0aa3fbf7 --- /dev/null +++ b/frontend/src/component/segments/SegmentTable/SegmentTable.tsx @@ -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 ( + + + + + + } + /> + } + isLoading={loading} + > + + + + + + + } + elseShow={() => ( + <> + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+ 0 + } + show={ + + No segments found matching “ + {globalFilter}” + + } + /> + + )} + /> +
+ ); +}; + +const COLUMNS = [ + { + id: 'Icon', + width: '1%', + canSort: false, + Cell: () => } />, + disableGlobalFilter: true, + }, + { + Header: 'Name', + accessor: 'name', + width: '80%', + Cell: ({ value, row: { original } }: any) => ( + + ), + }, + { + 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) => ( + + ), + }, + { + accessor: 'description', + }, +]; diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index 7426caed56..c7434a5a4f 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -2,7 +2,10 @@ exports[`renders an empty list correctly 1`] = ` [ -
+
@@ -109,7 +112,7 @@ exports[`renders an empty list correctly 1`] = ` className="tss-54jt3w-bodyContainer" > { const { data, error, mutate } = useSWR( [strategyId, uiConfig.flags], - fetchSegments + fetchSegments, + { + refreshInterval: 15 * 1000, + } ); const refetchSegments = useCallback(() => { diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 940701e919..ffd41259e3 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -45,6 +45,7 @@ export default createTheme({ borderRadiusLarge: '12px', borderRadiusExtraLarge: '20px', tableRowHeight: 64, + tableRowHeightCompact: 56, tableRowHeightDense: 48, }, palette: { diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index 29742fa030..3ea9f36bf1 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -109,6 +109,7 @@ declare module '@mui/system/createTheme/shape' { borderRadiusLarge: string; borderRadiusExtraLarge: string; tableRowHeight: number; + tableRowHeightCompact: number; tableRowHeightDense: number; } }