From 570e9f88be056d2a68767ff9033905dc9a76cd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 31 May 2022 07:59:09 +0100 Subject: [PATCH] feat: upgrade users table (#1040) * feat: upgrade users table * fix misc ui/ux bugs * refactor: address PR comments * fix: searching by `undefined` * fix: searching for undefined on invoices, table placeholder centering * refactor: abstract users list actions into new component * refactor: move styled components to top of files --- .../ReportStatusCell/ReportStatusCell.tsx | 16 +- .../Reporting/ReportTable/ReportTable.tsx | 39 +- .../BillingInformation/BillingInformation.tsx | 49 ++- .../BillingInformationButton.tsx | 10 +- .../BillingPlan/BillingPlan.tsx | 122 +++--- .../BillingPlan/GridColLink/GridColLink.tsx | 8 +- .../billing/BillingHistory/BillingHistory.tsx | 16 +- .../ProjectRoleList/ProjectRoleList.tsx | 2 + .../component/admin/users/UserAdmin.styles.ts | 18 - .../admin/users/UserForm/UserForm.tsx | 2 +- .../src/component/admin/users/UsersAdmin.tsx | 62 +-- .../UserListItem/UserListItem.styles.ts | 70 ---- .../UsersList/UserListItem/UserListItem.tsx | 173 -------- .../UsersList/UserTypeCell/UserTypeCell.tsx | 29 ++ .../UsersActionsCell/UsersActionsCell.tsx | 57 +++ .../admin/users/UsersList/UsersList.tsx | 389 ++++++++++-------- .../admin/users/UsersList/loadingData.ts | 77 ---- .../admin/users/hooks/useAddUserForm.ts | 2 +- .../src/component/common/GridRow/GridRow.tsx | 10 +- .../InstanceStatus/InstanceStatusBar.tsx | 84 ++-- .../common/StatusBadge/StatusBadge.tsx | 22 +- .../Table/TableActions/TableActions.styles.ts | 73 ---- .../Table/TableActions/TableActions.tsx | 86 ---- .../TableSearchField.styles.ts | 42 -- .../TableSearchField/TableSearchField.tsx | 74 ---- .../TableCellSortable.styles.ts | 40 -- .../TableCellSortable/TableCellSortable.tsx | 96 ----- .../TablePlaceholder.styles.ts | 2 +- .../cells/FeatureTypeCell/FeatureTypeCell.tsx | 2 +- .../Table/cells/TimeAgoCell/TimeAgoCell.tsx | 38 ++ .../context/ContextList/ContextList.tsx | 3 + .../EnvironmentTable/EnvironmentTable.tsx | 25 ++ .../FeatureView/FeatureType/FeatureType.tsx | 2 +- .../Project/ProjectInfo/ProjectInfo.styles.ts | 2 +- .../StrategiesList/StrategiesList.tsx | 3 + .../tags/TagTypeList/TagTypeList.tsx | 2 + .../hooks/api/getters/useUsers/useUsers.ts | 52 +-- frontend/src/hooks/useUsersFilter.ts | 56 --- frontend/src/hooks/useUsersPlan.ts | 5 +- frontend/src/hooks/useUsersSort.ts | 111 ----- frontend/src/utils/sortTypes.ts | 2 +- 41 files changed, 621 insertions(+), 1352 deletions(-) delete mode 100644 frontend/src/component/admin/users/UserAdmin.styles.ts delete mode 100644 frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts delete mode 100644 frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx create mode 100644 frontend/src/component/admin/users/UsersList/UserTypeCell/UserTypeCell.tsx create mode 100644 frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx delete mode 100644 frontend/src/component/admin/users/UsersList/loadingData.ts delete mode 100644 frontend/src/component/common/Table/TableActions/TableActions.styles.ts delete mode 100644 frontend/src/component/common/Table/TableActions/TableActions.tsx delete mode 100644 frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.styles.ts delete mode 100644 frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx delete mode 100644 frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts delete mode 100644 frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx create mode 100644 frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx delete mode 100644 frontend/src/hooks/useUsersFilter.ts delete mode 100644 frontend/src/hooks/useUsersSort.ts diff --git a/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx b/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx index 601e7def9f..ae4498b228 100644 --- a/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx +++ b/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx @@ -4,6 +4,14 @@ import { ReportProblemOutlined, Check } from '@mui/icons-material'; import { styled } from '@mui/material'; import { IReportTableRow } from 'component/Reporting/ReportTable/ReportTable'; +const StyledText = styled('span')(({ theme }) => ({ + display: 'flex', + gap: '1ch', + alignItems: 'center', + textAlign: 'right', + '& svg': { color: theme.palette.inactiveIcon }, +})); + interface IReportStatusCellProps { row: { original: IReportTableRow; @@ -33,11 +41,3 @@ export const ReportStatusCell: VFC = ({ ); }; - -const StyledText = styled('span')(({ theme }) => ({ - display: 'flex', - gap: '1ch', - alignItems: 'center', - textAlign: 'right', - '& svg': { color: theme.palette.inactiveIcon }, -})); diff --git a/frontend/src/component/Reporting/ReportTable/ReportTable.tsx b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx index f428942fc7..792c621b03 100644 --- a/frontend/src/component/Reporting/ReportTable/ReportTable.tsx +++ b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx @@ -3,6 +3,7 @@ import { TableSearch, SortableTableHeader, TableCell, + TablePlaceholder, } from 'component/common/Table'; import { PageContent } from 'component/common/PageContent/PageContent'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; @@ -24,6 +25,7 @@ import { import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExpiredAt'; import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; import theme from 'themes/theme'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; interface IReportTableProps { projectId: string; @@ -52,7 +54,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { const initialState = useMemo( () => ({ - hiddenColumns: ['description'], + hiddenColumns: [], sortBy: [{ id: 'name' }], }), [] @@ -83,9 +85,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { useEffect(() => { if (isSmallScreen) { - setHiddenColumns(['createdAt', 'expiredAt', 'description']); - } else { - setHiddenColumns(['description']); + setHiddenColumns(['createdAt', 'expiredAt']); } }, [setHiddenColumns, isSmallScreen]); @@ -101,6 +101,8 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { /> ); + console.log(rows); + return ( @@ -122,6 +124,27 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { + 0} + show={ + + No features found matching “ + {globalFilter} + ” + + } + elseShow={ + + No features available. Get started by adding a + new feature toggle. + + } + /> + } + /> ); }; @@ -149,12 +172,14 @@ const COLUMNS = [ sortType: 'date', align: 'center', Cell: FeatureSeenCell, + disableGlobalFilter: true, }, { Header: 'Type', accessor: 'type', align: 'center', Cell: FeatureTypeCell, + disableGlobalFilter: true, }, { Header: 'Feature toggle name', @@ -168,11 +193,13 @@ const COLUMNS = [ accessor: 'createdAt', sortType: 'date', Cell: DateCell, + disableGlobalFilter: true, }, { Header: 'Expired', accessor: 'expiredAt', Cell: ReportExpiredCell, + disableGlobalFilter: true, }, { Header: 'Status', @@ -185,8 +212,6 @@ const COLUMNS = [ accessor: 'stale', sortType: 'boolean', Cell: FeatureStaleCell, - }, - { - accessor: 'description', + disableGlobalFilter: true, }, ]; diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx index cca43c7ae2..5ca1a22328 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx @@ -4,6 +4,30 @@ import { BillingInformationButton } from './BillingInformationButton/BillingInfo import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { IInstanceStatus, InstanceState } from 'interfaces/instance'; +const StyledInfoBox = styled('aside')(({ theme }) => ({ + padding: theme.spacing(4), + height: '100%', + borderRadius: theme.shape.borderRadiusLarge, + backgroundColor: theme.palette.secondaryContainer, +})); + +const StyledTitle = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(4), +})); + +const StyledAlert = styled(Alert)(({ theme }) => ({ + marginBottom: theme.spacing(4), +})); + +const StyledInfoLabel = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.secondary, +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: `${theme.spacing(2.5)} 0`, + borderColor: theme.palette.dividerAlternative, +})); interface IBillingInformationProps { instanceStatus: IInstanceStatus; } @@ -43,28 +67,3 @@ export const BillingInformation: FC = ({ ); }; - -const StyledInfoBox = styled('aside')(({ theme }) => ({ - padding: theme.spacing(4), - height: '100%', - borderRadius: theme.shape.borderRadiusLarge, - backgroundColor: theme.palette.secondaryContainer, -})); - -const StyledTitle = styled(Typography)(({ theme }) => ({ - marginBottom: theme.spacing(4), -})); - -const StyledAlert = styled(Alert)(({ theme }) => ({ - marginBottom: theme.spacing(4), -})); - -const StyledInfoLabel = styled(Typography)(({ theme }) => ({ - fontSize: theme.fontSizes.smallBody, - color: theme.palette.text.secondary, -})); - -const StyledDivider = styled(Divider)(({ theme }) => ({ - margin: `${theme.spacing(2.5)} 0`, - borderColor: theme.palette.dividerAlternative, -})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx index 82e0ba1c87..8a3012a53a 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx @@ -16,6 +16,11 @@ Billing information:%0D%0A%0D%0A -- Thank you for signing up. We will upgrade your trial as quick as possible and we will grant you access to the application again. --`; +const StyledButton = styled(Button)(({ theme }) => ({ + width: '100%', + marginBottom: theme.spacing(1.5), +})); + interface IBillingInformationButtonProps { update?: boolean; } @@ -29,8 +34,3 @@ export const BillingInformationButton: VFC = ({ ); }; - -const StyledButton = styled(Button)(({ theme }) => ({ - width: '100%', - marginBottom: theme.spacing(1.5), -})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx index ca2acc67c4..537b56b807 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Alert, Divider, Grid, styled, Typography } from '@mui/material'; import { Link } from 'react-router-dom'; import CheckIcon from '@mui/icons-material/Check'; -import useUsers from 'hooks/api/getters/useUsers/useUsers'; +import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { IInstanceStatus, @@ -15,6 +15,66 @@ import { GridCol } from 'component/common/GridCol/GridCol'; import { GridColLink } from './GridColLink/GridColLink'; import { STRIPE } from 'component/admin/billing/flags'; +const StyledPlanBox = styled('aside')(({ theme }) => ({ + padding: theme.spacing(2.5), + height: '100%', + borderRadius: theme.shape.borderRadiusLarge, + boxShadow: theme.boxShadows.elevated, + [theme.breakpoints.up('md')]: { + padding: theme.spacing(6.5), + }, +})); + +const StyledInfoLabel = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.secondary, +})); + +const StyledPlanBadge = styled('span')(({ theme }) => ({ + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + borderRadius: theme.shape.borderRadiusLarge, + fontSize: theme.fontSizes.smallerBody, + backgroundColor: theme.palette.statusBadge.success, + color: theme.palette.success.dark, + fontWeight: theme.fontWeight.bold, +})); + +const StyledPlanSpan = styled('span')(({ theme }) => ({ + fontSize: '3.25rem', + lineHeight: 1, + color: theme.palette.primary.main, + fontWeight: 800, +})); + +const StyledTrialSpan = styled('span')(({ theme }) => ({ + marginLeft: theme.spacing(1.5), + fontWeight: theme.fontWeight.bold, +})); + +const StyledPriceSpan = styled('span')(({ theme }) => ({ + color: theme.palette.primary.main, + fontSize: theme.fontSizes.mainHeader, + fontWeight: theme.fontWeight.bold, +})); + +const StyledAlert = styled(Alert)(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + marginBottom: theme.spacing(3), + marginTop: theme.spacing(-1.5), + [theme.breakpoints.up('md')]: { + marginTop: theme.spacing(-4.5), + }, +})); + +const StyledCheckIcon = styled(CheckIcon)(({ theme }) => ({ + fontSize: '1rem', + marginRight: theme.spacing(1), +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: `${theme.spacing(3)} 0`, +})); + interface IBillingPlanProps { instanceStatus: IInstanceStatus; } @@ -194,63 +254,3 @@ export const BillingPlan: FC = ({ instanceStatus }) => { ); }; - -const StyledPlanBox = styled('aside')(({ theme }) => ({ - padding: theme.spacing(2.5), - height: '100%', - borderRadius: theme.shape.borderRadiusLarge, - boxShadow: theme.boxShadows.elevated, - [theme.breakpoints.up('md')]: { - padding: theme.spacing(6.5), - }, -})); - -const StyledInfoLabel = styled(Typography)(({ theme }) => ({ - fontSize: theme.fontSizes.smallBody, - color: theme.palette.text.secondary, -})); - -const StyledPlanBadge = styled('span')(({ theme }) => ({ - padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, - borderRadius: theme.shape.borderRadiusLarge, - fontSize: theme.fontSizes.smallerBody, - backgroundColor: theme.palette.statusBadge.success, - color: theme.palette.success.dark, - fontWeight: theme.fontWeight.bold, -})); - -const StyledPlanSpan = styled('span')(({ theme }) => ({ - fontSize: '3.25rem', - lineHeight: 1, - color: theme.palette.primary.main, - fontWeight: 800, -})); - -const StyledTrialSpan = styled('span')(({ theme }) => ({ - marginLeft: theme.spacing(1.5), - fontWeight: theme.fontWeight.bold, -})); - -const StyledPriceSpan = styled('span')(({ theme }) => ({ - color: theme.palette.primary.main, - fontSize: theme.fontSizes.mainHeader, - fontWeight: theme.fontWeight.bold, -})); - -const StyledAlert = styled(Alert)(({ theme }) => ({ - fontSize: theme.fontSizes.smallerBody, - marginBottom: theme.spacing(3), - marginTop: theme.spacing(-1.5), - [theme.breakpoints.up('md')]: { - marginTop: theme.spacing(-4.5), - }, -})); - -const StyledCheckIcon = styled(CheckIcon)(({ theme }) => ({ - fontSize: '1rem', - marginRight: theme.spacing(1), -})); - -const StyledDivider = styled(Divider)(({ theme }) => ({ - margin: `${theme.spacing(3)} 0`, -})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx index e6079a374e..d077a58c8b 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx @@ -1,11 +1,11 @@ import { styled } from '@mui/material'; import { FC } from 'react'; -export const GridColLink: FC = ({ children }) => { - return ({children}); -}; - const StyledSpan = styled('span')(({ theme }) => ({ fontSize: theme.fontSizes.smallBody, marginLeft: theme.spacing(1), })); + +export const GridColLink: FC = ({ children }) => { + return ({children}); +}; diff --git a/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx b/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx index 233108e9a1..5d53109448 100644 --- a/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx +++ b/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx @@ -16,6 +16,11 @@ import { Box, IconButton, styled, Typography } from '@mui/material'; import FileDownload from '@mui/icons-material/FileDownload'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +const StyledTitle = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(6), + marginBottom: theme.spacing(2.5), + fontSize: theme.fontSizes.mainHeader, +})); interface IBillingHistoryProps { data: Record[]; isLoading?: boolean; @@ -29,18 +34,19 @@ const columns = [ { Header: 'Status', accessor: 'status', + disableGlobalFilter: true, }, { Header: 'Due date', accessor: 'dueDate', Cell: DateCell, sortType: 'date', + disableGlobalFilter: true, }, { Header: 'Download', accessor: 'invoicePDF', align: 'center', - disableSortBy: true, Cell: ({ value }: { value: string }) => ( ), width: 100, + disableGlobalFilter: true, + disableSortBy: true, }, ]; @@ -110,9 +118,3 @@ export const BillingHistory: VFC = ({ ); }; - -const StyledTitle = styled(Typography)(({ theme }) => ({ - marginTop: theme.spacing(6), - marginBottom: theme.spacing(2.5), - fontSize: theme.fontSizes.mainHeader, -})); diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx index 29e80877cf..cebedfd551 100644 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx @@ -73,6 +73,7 @@ const ProjectRoleList = () => { icon={} /> ), + disableGlobalFilter: true, }, { Header: 'Project role', @@ -133,6 +134,7 @@ const ProjectRoleList = () => { ), width: 100, + disableGlobalFilter: true, disableSortBy: true, }, ], diff --git a/frontend/src/component/admin/users/UserAdmin.styles.ts b/frontend/src/component/admin/users/UserAdmin.styles.ts deleted file mode 100644 index a93b3c7f2b..0000000000 --- a/frontend/src/component/admin/users/UserAdmin.styles.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - userListBody: { - padding: theme.spacing(4), - paddingBottom: '4rem', - minHeight: '50vh', - position: 'relative', - }, - tableActions: { - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - '&>button': { - flexShrink: 0, - }, - }, -})); diff --git a/frontend/src/component/admin/users/UserForm/UserForm.tsx b/frontend/src/component/admin/users/UserForm/UserForm.tsx index b5bdf0b278..e4f5aa5c1a 100644 --- a/frontend/src/component/admin/users/UserForm/UserForm.tsx +++ b/frontend/src/component/admin/users/UserForm/UserForm.tsx @@ -10,7 +10,7 @@ import { } from '@mui/material'; import { useStyles } from './UserForm.styles'; import React from 'react'; -import useUsers from 'hooks/api/getters/useUsers/useUsers'; +import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { EDIT } from 'constants/misc'; import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap'; diff --git a/frontend/src/component/admin/users/UsersAdmin.tsx b/frontend/src/component/admin/users/UsersAdmin.tsx index f69ea8f64a..e34f238537 100644 --- a/frontend/src/component/admin/users/UsersAdmin.tsx +++ b/frontend/src/component/admin/users/UsersAdmin.tsx @@ -1,72 +1,22 @@ -import { useContext, useState } from 'react'; +import { useContext } from 'react'; import UsersList from './UsersList/UsersList'; import AdminMenu from '../menu/AdminMenu'; -import { PageContent } from 'component/common/PageContent/PageContent'; import AccessContext from 'contexts/AccessContext'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ADMIN } from 'component/providers/AccessProvider/permissions'; -import { Button } from '@mui/material'; -import { TableActions } from 'component/common/Table/TableActions/TableActions'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { useStyles } from './UserAdmin.styles'; -import { useNavigate } from 'react-router-dom'; import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; const UsersAdmin = () => { - const [search, setSearch] = useState(''); const { hasAccess } = useContext(AccessContext); - const navigate = useNavigate(); - const { classes: styles } = useStyles(); return (
- - - setSearch(search) - } - /> - -
- } - elseShow={ - - PS! Only admins can add/remove users. - - } - /> - } - /> - } - > - } - elseShow={} - /> - + } + elseShow={} + /> ); }; diff --git a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts deleted file mode 100644 index 026abb9f0d..0000000000 --- a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - tableRow: { - '& > td': { - padding: '4px 16px', - borderColor: theme.palette.grey[300], - }, - '&:hover': { - backgroundColor: theme.palette.grey[100], - }, - }, - tableCellHeader: { - '& > th': { - backgroundColor: theme.palette.grey[200], - fontWeight: 'normal', - border: 0, - '&:first-of-type': { - '&, & > button': { - borderTopLeftRadius: theme.spacing(1), - borderBottomLeftRadius: theme.spacing(1), - }, - }, - '&:last-of-type': { - '&, & > button': { - borderTopRightRadius: theme.spacing(1), - borderBottomRightRadius: theme.spacing(1), - }, - }, - }, - }, - errorMessage: { - textAlign: 'center', - marginTop: '20vh', - }, - leftTableCell: { - textAlign: 'left', - }, - shrinkTableCell: { - whiteSpace: 'nowrap', - width: '0.1%', - }, - avatar: { - width: '32px', - height: '32px', - margin: 'auto', - }, - firstColumnSM: { - [theme.breakpoints.down('md')]: { - borderTopLeftRadius: '8px', - borderBottomLeftRadius: '8px', - }, - }, - firstColumnXS: { - [theme.breakpoints.down('sm')]: { - borderTopLeftRadius: '8px', - borderBottomLeftRadius: '8px', - }, - }, - hideSM: { - [theme.breakpoints.down('md')]: { - display: 'none', - }, - }, - hideXS: { - [theme.breakpoints.down('sm')]: { - display: 'none', - }, - }, -})); diff --git a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx deleted file mode 100644 index ff8a9f140f..0000000000 --- a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - Avatar, - IconButton, - TableCell, - TableRow, - Tooltip, - Typography, -} from '@mui/material'; -import classnames from 'classnames'; -import { Delete, Edit, Lock, MonetizationOn } from '@mui/icons-material'; -import { SyntheticEvent, useContext } from 'react'; -import { ADMIN } from 'component/providers/AccessProvider/permissions'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import AccessContext from 'contexts/AccessContext'; -import { IUser } from 'interfaces/user'; -import { useNavigate } from 'react-router-dom'; -import { ILocationSettings } from 'hooks/useLocationSettings'; -import { formatDateYMD } from 'utils/formatDate'; -import { Highlighter } from 'component/common/Highlighter/Highlighter'; -import { useStyles } from './UserListItem.styles'; -import TimeAgo from 'react-timeago'; - -interface IUserListItemProps { - user: IUser; - renderRole: (roleId: number) => string; - openPwDialog: (user: IUser) => (e: SyntheticEvent) => void; - openDelDialog: (user: IUser) => (e: SyntheticEvent) => void; - locationSettings: ILocationSettings; - search: string; - isBillingUsers?: boolean; -} - -const UserListItem = ({ - user, - renderRole, - openDelDialog, - openPwDialog, - locationSettings, - search, - isBillingUsers, -}: IUserListItemProps) => { - const { hasAccess } = useContext(AccessContext); - const navigate = useNavigate(); - const { classes: styles } = useStyles(); - - const renderTimeAgo = (date: string) => ( - - - - - - ); - - return ( - - - - ({ - color: theme.palette.primary.light, - fontSize: '1.75rem', - })} - /> - - } - elseShow={Free} - /> - - } - /> - - - {formatDateYMD(user.createdAt, locationSettings.locale)} - - - - - - - - {user.name} - - - - - - {user.username || user.email} - - - - - - {renderRole(user.rootRole)} - - - - renderTimeAgo(user.seenAt!)} - elseShow={ - - Never logged - - } - /> - - - - - navigate(`/admin/users/${user.id}/edit`) - } - size="large" - > - - - - - - - - - - - - - - - } - elseShow={} - /> - - ); -}; - -export default UserListItem; diff --git a/frontend/src/component/admin/users/UsersList/UserTypeCell/UserTypeCell.tsx b/frontend/src/component/admin/users/UsersList/UserTypeCell/UserTypeCell.tsx new file mode 100644 index 0000000000..d27d5b205d --- /dev/null +++ b/frontend/src/component/admin/users/UsersList/UserTypeCell/UserTypeCell.tsx @@ -0,0 +1,29 @@ +import { MonetizationOn } from '@mui/icons-material'; +import { styled, Tooltip } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; + +const StyledMonetizationOn = styled(MonetizationOn)(({ theme }) => ({ + color: theme.palette.primary.light, + fontSize: '1.75rem', +})); + +interface IUserTypeCellProps { + value: boolean; +} + +export const UserTypeCell = ({ value }: IUserTypeCellProps) => { + return ( + + + + + } + elseShow="Free" + /> + + ); +}; diff --git a/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx b/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx new file mode 100644 index 0000000000..720b2679ea --- /dev/null +++ b/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx @@ -0,0 +1,57 @@ +import { Delete, Edit, Lock } from '@mui/icons-material'; +import { Box, styled } from '@mui/material'; +import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; +import { ADMIN } from 'component/providers/AccessProvider/permissions'; +import { VFC } from 'react'; + +const StyledBox = styled(Box)(() => ({ + display: 'flex', + justifyContent: 'center', +})); + +interface IUsersActionsCellProps { + onEdit: (event: React.SyntheticEvent) => void; + onChangePassword: (event: React.SyntheticEvent) => void; + onDelete: (event: React.SyntheticEvent) => void; +} + +export const UsersActionsCell: VFC = ({ + onEdit, + onChangePassword, + onDelete, +}) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index e88a6d0054..3c37da0794 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -1,49 +1,52 @@ /* eslint-disable no-alert */ -import React, { useContext, useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Table, + SortableTableHeader, TableBody, TableCell, - TableHead, TableRow, -} from '@mui/material'; -import classnames from 'classnames'; + TablePlaceholder, + TableSearch, +} from 'component/common/Table'; import ChangePassword from './ChangePassword/ChangePassword'; import DeleteUser from './DeleteUser/DeleteUser'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import AccessContext from 'contexts/AccessContext'; -import { ADMIN } from 'component/providers/AccessProvider/permissions'; import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded'; -import useUsers from 'hooks/api/getters/useUsers/useUsers'; +import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi'; -import UserListItem from './UserListItem/UserListItem'; -import loadingData from './loadingData'; -import useLoading from 'hooks/useLoading'; -import usePagination from 'hooks/usePagination'; -import PaginateUI from 'component/common/PaginateUI/PaginateUI'; import { IUser } from 'interfaces/user'; import IRole from 'interfaces/role'; import useToast from 'hooks/useToast'; -import { useLocationSettings } from 'hooks/useLocationSettings'; import { formatUnknownError } from 'utils/formatUnknownError'; -import { useUsersFilter } from 'hooks/useUsersFilter'; -import { useUsersSort } from 'hooks/useUsersSort'; -import { TableCellSortable } from 'component/common/Table/TableCellSortable/TableCellSortable'; -import { useStyles } from './UserListItem/UserListItem.styles'; import { useUsersPlan } from 'hooks/useUsersPlan'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import { Avatar, Button, styled, useMediaQuery } from '@mui/material'; +import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { UserTypeCell } from './UserTypeCell/UserTypeCell'; +import { useGlobalFilter, useSortBy, useTable } from 'react-table'; +import { sortTypes } from 'utils/sortTypes'; +import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +import { useNavigate } from 'react-router-dom'; +import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; +import theme from 'themes/theme'; +import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; +import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell'; -interface IUsersListProps { - search: string; -} +const StyledAvatar = styled(Avatar)(({ theme }) => ({ + width: theme.spacing(4), + height: theme.spacing(4), + margin: 'auto', +})); -const UsersList = ({ search }: IUsersListProps) => { - const { classes: styles } = useStyles(); +const UsersList = () => { + const navigate = useNavigate(); const { users, roles, refetch, loading } = useUsers(); const { setToastData, setToastApiError } = useToast(); const { removeUser, changePassword, userLoading, userApiErrors } = useAdminUsersApi(); - const { hasAccess } = useContext(AccessContext); - const { locationSettings } = useLocationSettings(); const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({ open: false, }); @@ -52,28 +55,10 @@ const UsersList = ({ search }: IUsersListProps) => { const [emailSent, setEmailSent] = useState(false); const [inviteLink, setInviteLink] = useState(''); const [delUser, setDelUser] = useState(); - const ref = useLoading(loading); const { planUsers, isBillingUsers } = useUsersPlan(users); - const { filtered, setFilter } = useUsersFilter(planUsers); - const { sorted, sort, setSort } = useUsersSort(filtered); - const filterUsersByQueryPage = (user: IUser) => { - const fieldsToSearch = [ - user.name ?? '', - user.username ?? user.email ?? '', - ]; - - return fieldsToSearch.some(field => { - return field.toLowerCase().includes(search.toLowerCase()); - }); - }; - // Filter users and reset pagination page when search is triggered - const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } = - usePagination(sorted, 50, filterUsersByQueryPage); - - useEffect(() => { - setFilter(filter => ({ ...filter, query: search })); - }, [search, setFilter]); + const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const closeDelDialog = () => { setDelDialog(false); @@ -116,136 +101,208 @@ const UsersList = ({ search }: IUsersListProps) => { setInviteLink(''); }; - const renderRole = (roleId: number) => { - const role = roles.find((r: IRole) => r.id === roleId); - return role ? role.name : ''; - }; + const columns = useMemo( + () => [ + { + id: 'type', + Header: 'Type', + accessor: 'paid', + Cell: ({ row: { original: user } }: any) => ( + + ), + disableGlobalFilter: true, + sortType: 'boolean', + }, + { + Header: 'Created', + accessor: 'createdAt', + Cell: DateCell, + disableGlobalFilter: true, + sortType: 'date', + }, + { + Header: 'Avatar', + accessor: 'imageUrl', + Cell: ({ row: { original: user } }: any) => ( + + + + ), + disableGlobalFilter: true, + disableSortBy: true, + }, + { + Header: 'Name', + accessor: (row: any) => row.name || '', + width: '40%', + Cell: HighlightCell, + }, + { + id: 'username', + Header: 'Username', + accessor: (row: any) => row.username || row.email, + width: '40%', + Cell: HighlightCell, + }, + { + id: 'role', + Header: 'Role', + accessor: (row: any) => + roles.find((role: IRole) => role.id === row.rootRole) + ?.name || '', + disableGlobalFilter: true, + }, + { + id: 'last-login', + Header: 'Last login', + accessor: (row: any) => row.seenAt || '', + Cell: ({ row: { original: user } }: any) => ( + + ), + disableGlobalFilter: true, + sortType: 'date', + }, + { + Header: 'Actions', + id: 'Actions', + align: 'center', + Cell: ({ row: { original: user } }: any) => ( + { + navigate(`/admin/users/${user.id}/edit`); + }} + onChangePassword={openPwDialog(user)} + onDelete={openDelDialog(user)} + /> + ), + width: 100, + disableGlobalFilter: true, + disableSortBy: true, + }, + ], + [roles, navigate, isBillingUsers] + ); - const renderUsers = () => { - if (loading) { - return loadingData.map(user => ( - - )); + const initialState = useMemo(() => { + return { + sortBy: [{ id: 'createdAt', desc: false }], + hiddenColumns: isBillingUsers ? [] : ['type'], + }; + }, [isBillingUsers]); + + const data = isBillingUsers ? planUsers : users; + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { globalFilter }, + setGlobalFilter, + setHiddenColumns, + } = useTable( + { + columns: columns as any[], // TODO: fix after `react-table` v8 update + data, + initialState, + sortTypes, + autoResetGlobalFilter: false, + autoResetSortBy: false, + disableSortRemove: true, + defaultColumn: { + Cell: TextCell, + }, + }, + useGlobalFilter, + useSortBy + ); + + useEffect(() => { + const hiddenColumns = []; + if (!isBillingUsers || isSmallScreen) { + hiddenColumns.push('type'); } - - return page.map(user => { - return ( - - ); - }); - }; - - if (!users) return null; + if (isSmallScreen) { + hiddenColumns.push(...['createdAt', 'username']); + } + if (isExtraSmallScreen) { + hiddenColumns.push(...['imageUrl', 'role', 'last-login']); + } + setHiddenColumns(hiddenColumns); + }, [setHiddenColumns, isExtraSmallScreen, isSmallScreen, isBillingUsers]); return ( -
- - - - - Type - - } - /> - - Created - - - Avatar - - - Name - - - Username - - - Role - - - Last login - - - {hasAccess(ADMIN) ? 'Actions' : ''} - - - - {renderUsers()} - + + + + + } /> -
+ } + > + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
0} + condition={rows.length === 0} show={ -

- There are no results for "{search}" -

+ 0} + show={ + + No users found matching “ + {globalFilter} + ” + + } + elseShow={ + + No users available. Get started by adding one. + + } + /> } /> -
{ /> } /> -
+ ); }; diff --git a/frontend/src/component/admin/users/UsersList/loadingData.ts b/frontend/src/component/admin/users/UsersList/loadingData.ts deleted file mode 100644 index a197369849..0000000000 --- a/frontend/src/component/admin/users/UsersList/loadingData.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { IUser } from 'interfaces/user'; - -const loadingData: IUser[] = [ - { - id: 1, - username: 'admin', - email: 'some-email@email.com', - name: 'admin', - permissions: ['ADMIN'], - imageUrl: - 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro', - seenAt: null, - loginAttempts: 0, - createdAt: '2021-04-21T12:09:55.923Z', - rootRole: 1, - inviteLink: '', - isAPI: false, - }, - { - id: 16, - name: 'test', - email: 'test@test.no', - permissions: [], - imageUrl: - 'https://gravatar.com/avatar/879fdbb54e4a6cdba456fcb11abe5971?size=42&default=retro', - seenAt: null, - loginAttempts: 0, - createdAt: '2021-04-21T15:54:02.765Z', - rootRole: 2, - inviteLink: '', - isAPI: false, - }, - { - id: 3, - name: 'Testesen', - email: 'test@test.com', - permissions: [], - imageUrl: - 'https://gravatar.com/avatar/6c15d63f08137733ec0828cd0a3a5dc4?size=42&default=retro', - seenAt: '2021-04-21T14:34:31.515Z', - loginAttempts: 0, - createdAt: '2021-04-21T12:33:17.712Z', - rootRole: 1, - inviteLink: '', - isAPI: false, - }, - { - id: 4, - name: 'test', - email: 'test@test.io', - permissions: [], - imageUrl: - 'https://gravatar.com/avatar/879fdbb54e4a6cdba456fcb11abe5971?size=42&default=retro', - seenAt: null, - loginAttempts: 0, - createdAt: '2021-04-21T15:54:02.765Z', - rootRole: 2, - inviteLink: '', - isAPI: false, - }, - { - id: 5, - name: 'Testesen', - email: 'test@test.uk', - permissions: [], - imageUrl: - 'https://gravatar.com/avatar/6c15d63f08137733ec0828cd0a3a5dc4?size=42&default=retro', - seenAt: '2021-04-21T14:34:31.515Z', - loginAttempts: 0, - createdAt: '2021-04-21T12:33:17.712Z', - rootRole: 1, - inviteLink: '', - isAPI: false, - }, -]; - -export default loadingData; diff --git a/frontend/src/component/admin/users/hooks/useAddUserForm.ts b/frontend/src/component/admin/users/hooks/useAddUserForm.ts index 11e0563b77..d17581a6cb 100644 --- a/frontend/src/component/admin/users/hooks/useAddUserForm.ts +++ b/frontend/src/component/admin/users/hooks/useAddUserForm.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import useUiBootstrap from 'hooks/api/getters/useUiBootstrap/useUiBootstrap'; -import useUsers from 'hooks/api/getters/useUsers/useUsers'; +import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; const useCreateUserForm = ( initialName = '', diff --git a/frontend/src/component/common/GridRow/GridRow.tsx b/frontend/src/component/common/GridRow/GridRow.tsx index a8c100fedb..54110f4cb3 100644 --- a/frontend/src/component/common/GridRow/GridRow.tsx +++ b/frontend/src/component/common/GridRow/GridRow.tsx @@ -1,6 +1,11 @@ import { Grid, styled, SxProps, Theme } from '@mui/material'; import { FC } from 'react'; +const StyledGrid = styled(Grid)(({ theme }) => ({ + flexWrap: 'nowrap', + gap: theme.spacing(1), +})); + export const GridRow: FC<{ sx?: SxProps }> = ({ sx, children }) => { return ( }> = ({ sx, children }) => { ); }; - -const StyledGrid = styled(Grid)(({ theme }) => ({ - flexWrap: 'nowrap', - gap: theme.spacing(1), -})); diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx index 5d1c6bff6a..7737234236 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx @@ -9,6 +9,48 @@ import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { calculateTrialDaysRemaining } from 'utils/billing'; +const StyledWarningBar = styled('aside')(({ theme }) => ({ + position: 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(1), + gap: theme.spacing(1), + borderBottom: '1px solid', + borderColor: theme.palette.warning.border, + background: theme.palette.warning.light, + color: theme.palette.warning.dark, +})); + +const StyledInfoBar = styled('aside')(({ theme }) => ({ + position: 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(1), + gap: theme.spacing(1), + borderBottom: '1px solid', + borderColor: theme.palette.info.border, + background: theme.palette.info.light, + color: theme.palette.info.dark, +})); + +const StyledButton = styled(Button)(({ theme }) => ({ + whiteSpace: 'nowrap', + minWidth: '8rem', + marginLeft: theme.spacing(2), +})); + +const StyledWarningIcon = styled(WarningAmber)(({ theme }) => ({ + color: theme.palette.warning.main, +})); + +const StyledInfoIcon = styled(InfoOutlined)(({ theme }) => ({ + color: theme.palette.info.main, +})); + interface IInstanceStatusBarProps { instanceStatus: IInstanceStatus; } @@ -85,45 +127,3 @@ const UpgradeButton = () => { ); }; - -const StyledWarningBar = styled('aside')(({ theme }) => ({ - position: 'relative', - zIndex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(1), - gap: theme.spacing(1), - borderBottom: '1px solid', - borderColor: theme.palette.warning.border, - background: theme.palette.warning.light, - color: theme.palette.warning.dark, -})); - -const StyledInfoBar = styled('aside')(({ theme }) => ({ - position: 'relative', - zIndex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(1), - gap: theme.spacing(1), - borderBottom: '1px solid', - borderColor: theme.palette.info.border, - background: theme.palette.info.light, - color: theme.palette.info.dark, -})); - -const StyledButton = styled(Button)(({ theme }) => ({ - whiteSpace: 'nowrap', - minWidth: '8rem', - marginLeft: theme.spacing(2), -})); - -const StyledWarningIcon = styled(WarningAmber)(({ theme }) => ({ - color: theme.palette.warning.main, -})); - -const StyledInfoIcon = styled(InfoOutlined)(({ theme }) => ({ - color: theme.palette.info.main, -})); diff --git a/frontend/src/component/common/StatusBadge/StatusBadge.tsx b/frontend/src/component/common/StatusBadge/StatusBadge.tsx index 7f026e82cc..ad02c63dbc 100644 --- a/frontend/src/component/common/StatusBadge/StatusBadge.tsx +++ b/frontend/src/component/common/StatusBadge/StatusBadge.tsx @@ -1,6 +1,17 @@ import { styled, useTheme } from '@mui/material'; import { ReactNode } from 'react'; +const StyledStatusBadge = styled('div')(({ theme }) => ({ + padding: theme.spacing(0.5, 1), + textDecoration: 'none', + color: theme.palette.text.primary, + display: 'inline-block', + borderRadius: theme.shape.borderRadius, + marginLeft: theme.spacing(1.5), + fontSize: theme.fontSizes.smallerBody, + lineHeight: 1, +})); + interface IStatusBadgeProps { severity: 'success' | 'warning'; className?: string; @@ -21,14 +32,3 @@ export const StatusBadge = ({ ); }; - -const StyledStatusBadge = styled('div')(({ theme }) => ({ - padding: theme.spacing(0.5, 1), - textDecoration: 'none', - color: theme.palette.text.primary, - display: 'inline-block', - borderRadius: theme.shape.borderRadius, - marginLeft: theme.spacing(1.5), - fontSize: theme.fontSizes.smallerBody, - lineHeight: 1, -})); diff --git a/frontend/src/component/common/Table/TableActions/TableActions.styles.ts b/frontend/src/component/common/Table/TableActions/TableActions.styles.ts deleted file mode 100644 index 65133e3555..0000000000 --- a/frontend/src/component/common/Table/TableActions/TableActions.styles.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - tableActions: { - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - '&>button': { - padding: theme.spacing(1), - flexShrink: 0, - }, - paddingRight: theme.spacing(1), - }, - fieldWidth: { - width: '45px', - '& .search-icon': { - marginRight: 0, - }, - '& .input-container, .clear-container': { - width: 0, - }, - '& input::placeholder': { - color: 'transparent', - transition: 'color 0.6s', - }, - '& input:focus-within::placeholder': { - color: theme.palette.text.primary, - }, - }, - fieldWidthEnter: { - width: '250px', - transition: 'width 0.6s', - '& .search-icon': { - marginRight: '8px', - }, - '& .input-container': { - width: '100%', - transition: 'width 0.6s', - }, - '& .clear-container': { - width: '30px', - transition: 'width 0.6s', - }, - '& .search-container': { - borderColor: theme.palette.grey[300], - }, - }, - fieldWidthLeave: { - width: '45px', - transition: 'width 0.6s', - '& .search-icon': { - marginRight: 0, - transition: 'margin-right 0.6s', - }, - '& .input-container, .clear-container': { - width: 0, - transition: 'width 0.6s', - }, - '& .search-container': { - borderColor: 'transparent', - }, - }, - verticalSeparator: { - height: '100%', - backgroundColor: theme.palette.grey[500], - width: '1px', - display: 'inline-block', - marginLeft: theme.spacing(2), - marginRight: theme.spacing(4), - padding: '10px 0', - verticalAlign: 'middle', - }, -})); diff --git a/frontend/src/component/common/Table/TableActions/TableActions.tsx b/frontend/src/component/common/Table/TableActions/TableActions.tsx deleted file mode 100644 index 1986a01417..0000000000 --- a/frontend/src/component/common/Table/TableActions/TableActions.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { FC, useState } from 'react'; -import { IconButton, Tooltip } from '@mui/material'; -import { Search } from '@mui/icons-material'; -import { useAsyncDebounce } from 'react-table'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import AnimateOnMount from 'component/common/AnimateOnMount/AnimateOnMount'; -import { TableSearchField } from './TableSearchField/TableSearchField'; -import { useStyles } from './TableActions.styles'; - -interface ITableActionsProps { - initialSearchValue?: string; - onSearch?: (value: string) => void; - searchTip?: string; - isSeparated?: boolean; -} - -/** - * @deprecated Use instead - */ -export const TableActions: FC = ({ - initialSearchValue: search, - onSearch = () => {}, - searchTip = 'Search', - children, - isSeparated, -}) => { - const [searchExpanded, setSearchExpanded] = useState(Boolean(search)); - const [searchInputState, setSearchInputState] = useState(search); - const [animating, setAnimating] = useState(false); - const debouncedOnSearch = useAsyncDebounce(onSearch, 200); - - const { classes: styles } = useStyles(); - - const onBlur = (clear = false) => { - if (!searchInputState || clear) { - setSearchExpanded(false); - } - }; - - const onSearchChange = (value: string) => { - debouncedOnSearch(value); - setSearchInputState(value); - }; - - return ( -
- - setAnimating(true)} - onEnd={() => setAnimating(false)} - > - - - - setSearchExpanded(true)} - size="large" - > - - - - } - /> - - } - /> - {children} -
- ); -}; diff --git a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.styles.ts b/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.styles.ts deleted file mode 100644 index ca59daa89d..0000000000 --- a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.styles.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - container: { - display: 'flex', - alignItems: 'center', - flexWrap: 'wrap', - gap: '1rem', - }, - search: { - display: 'flex', - alignItems: 'center', - backgroundColor: theme.palette.background.paper, - border: `1px solid ${theme.palette.grey[300]}`, - borderRadius: theme.shape.borderRadiusExtraLarge, - padding: '3px 5px 3px 12px', - maxWidth: '450px', - [theme.breakpoints.down('sm')]: { - width: '100%', - }, - '&.search-container:focus-within': { - borderColor: theme.palette.primary.light, - boxShadow: theme.boxShadows.main, - }, - }, - searchIcon: { - marginRight: 8, - color: theme.palette.inactiveIcon, - }, - clearContainer: { - width: '30px', - '& > button': { - padding: '7px', - }, - }, - clearIcon: { - fontSize: '18px', - }, - inputRoot: { - width: '100%', - }, -})); diff --git a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx b/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx deleted file mode 100644 index b6412d9b41..0000000000 --- a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { IconButton, InputBase, Tooltip } from '@mui/material'; -import { Search, Close } from '@mui/icons-material'; -import classnames from 'classnames'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from './TableSearchField.styles'; - -interface ITableSearchFieldProps { - value?: string; - onChange: (value: string) => void; - className?: string; - placeholder?: string; - onBlur?: (clear?: boolean) => void; -} - -export const TableSearchField = ({ - value = '', - onChange, - className, - placeholder, - onBlur, -}: ITableSearchFieldProps) => { - const { classes: styles } = useStyles(); - const placeholderText = placeholder ?? 'Search...'; - - return ( -
-
- - onChange(e.target.value)} - onBlur={() => onBlur?.()} - /> -
- - { - onChange(''); - onBlur?.(true); - }} - > - - - - } - /> -
-
-
- ); -}; diff --git a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts deleted file mode 100644 index 6697522f23..0000000000 --- a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - tableCellHeaderSortable: { - padding: 0, - cursor: 'pointer', - '& > svg': { - fontSize: 18, - verticalAlign: 'middle', - color: theme.palette.grey[700], - marginLeft: '4px', - }, - '&.sorted': { - fontWeight: 'bold', - '& > svg': { - color: theme.palette.grey[900], - }, - }, - }, - sortButton: { - all: 'unset', - padding: theme.spacing(2), - width: '100%', - '&:focus-visible, &:active': { - outline: 'revert', - }, - display: 'flex', - alignItems: 'center', - '&:hover': { - backgroundColor: theme.palette.grey[400], - '& > svg': { - color: theme.palette.grey[900], - }, - }, - }, - icon: { - marginLeft: theme.spacing(0.5), - fontSize: 18, - }, -})); diff --git a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx deleted file mode 100644 index 4cc3f1cd58..0000000000 --- a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { ReactNode, useContext } from 'react'; -import { TableCell } from '@mui/material'; -import classnames from 'classnames'; -import { - UnfoldMoreOutlined, - KeyboardArrowDown, - KeyboardArrowUp, -} from '@mui/icons-material'; -import { IUsersSort, UsersSortType } from 'hooks/useUsersSort'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from 'component/common/Table/TableCellSortable/TableCellSortable.styles'; -import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext'; - -// Add others as needed, e.g. UsersSortType | FeaturesSortType -type SortType = UsersSortType; -type Sort = IUsersSort; - -interface ITableCellSortableProps { - className?: string; - name: SortType; - sort: Sort; - setSort: React.Dispatch>; - children: ReactNode; -} - -/** - * @deprecated No longer in use. See `SortableTableHeader`. Remove when Users table is refactored. - */ -export const TableCellSortable = ({ - className, - name, - sort, - setSort, - children, -}: ITableCellSortableProps) => { - const { setAnnouncement } = useContext(AnnouncerContext); - const { classes: styles } = useStyles(); - - const ariaSort = - sort.type === name - ? sort.desc - ? 'descending' - : 'ascending' - : undefined; - - const onSortClick = () => { - setSort(prev => ({ - desc: !Boolean(prev.desc), - type: name, - })); - setAnnouncement( - `Sorted table by ${name}, ${sort.desc ? 'ascending' : 'descending'}` - ); - }; - - return ( - - - - ); -}; diff --git a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts index e85267f6ff..87eb009842 100644 --- a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts +++ b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts @@ -6,7 +6,7 @@ export const useStyles = makeStyles()(theme => ({ padding: '0.8rem', textAlign: 'center', display: 'flex', - justifyContent: 'space-between', + justifyContent: 'center', alignItems: 'center', marginTop: theme.spacing(2), }, diff --git a/frontend/src/component/common/Table/cells/FeatureTypeCell/FeatureTypeCell.tsx b/frontend/src/component/common/Table/cells/FeatureTypeCell/FeatureTypeCell.tsx index e4463fb8e5..617d2d3260 100644 --- a/frontend/src/component/common/Table/cells/FeatureTypeCell/FeatureTypeCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureTypeCell/FeatureTypeCell.tsx @@ -21,7 +21,7 @@ export const FeatureTypeCell: VFC = ({ value }) => { return (
- +
diff --git a/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx b/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx new file mode 100644 index 0000000000..484974e42d --- /dev/null +++ b/frontend/src/component/common/Table/cells/TimeAgoCell/TimeAgoCell.tsx @@ -0,0 +1,38 @@ +import { Tooltip, Typography } from '@mui/material'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { VFC } from 'react'; +import { formatDateYMD } from 'utils/formatDate'; +import { TextCell } from '../TextCell/TextCell'; +import TimeAgo from 'react-timeago'; + +interface ITimeAgoCellProps { + value?: string | number | Date; + live?: boolean; + emptyText?: string; +} + +export const TimeAgoCell: VFC = ({ + value, + live = false, + emptyText, +}) => { + const { locationSettings } = useLocationSettings(); + + if (!value) return {emptyText}; + + return ( + + + + + + + + ); +}; diff --git a/frontend/src/component/context/ContextList/ContextList.tsx b/frontend/src/component/context/ContextList/ContextList.tsx index 83ca142dfc..c90633f327 100644 --- a/frontend/src/component/context/ContextList/ContextList.tsx +++ b/frontend/src/component/context/ContextList/ContextList.tsx @@ -54,6 +54,7 @@ const ContextList: VFC = () => { { id: 'Icon', Cell: () => } />, + disableGlobalFilter: true, }, { Header: 'Name', @@ -90,6 +91,7 @@ const ContextList: VFC = () => { /> ), width: 150, + disableGlobalFilter: true, disableSortBy: true, }, { @@ -98,6 +100,7 @@ const ContextList: VFC = () => { }, { accessor: 'sortOrder', + disableGlobalFilter: true, sortType: 'number', }, ], diff --git a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx index ed4963d40f..ca95360e7c 100644 --- a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx +++ b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx @@ -7,6 +7,7 @@ import { TableSearch, SortableTableHeader, Table, + TablePlaceholder, } from 'component/common/Table'; import { useCallback } from 'react'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; @@ -22,6 +23,7 @@ import useEnvironmentApi, { createSortOrderPayload, } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import { formatUnknownError } from 'utils/formatUnknownError'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; export const EnvironmentTable = () => { const { changeSortOrder } = useEnvironmentApi(); @@ -97,6 +99,27 @@ export const EnvironmentTable = () => { + 0} + show={ + + No environments found matching “ + {globalFilter} + ” + + } + elseShow={ + + No environments available. Get started by adding + one. + + } + /> + } + /> ); }; @@ -106,6 +129,7 @@ const COLUMNS = [ id: 'Icon', canSort: false, Cell: () => } />, + disableGlobalFilter: true, }, { Header: 'Name', @@ -124,5 +148,6 @@ const COLUMNS = [ Cell: ({ row: { original } }: any) => ( ), + disableGlobalFilter: true, }, ]; diff --git a/frontend/src/component/feature/FeatureView/FeatureType/FeatureType.tsx b/frontend/src/component/feature/FeatureView/FeatureType/FeatureType.tsx index eba3f3ceca..161d2f9659 100644 --- a/frontend/src/component/feature/FeatureView/FeatureType/FeatureType.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureType/FeatureType.tsx @@ -16,7 +16,7 @@ const FeatureStatus = ({ type }: IFeatureTypeProps) => { const title = `"${typeName || type}" toggle`; return ( - + ); diff --git a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts index 63cc22b5b0..de253c67ba 100644 --- a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts +++ b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts @@ -95,7 +95,7 @@ export const useStyles = makeStyles()(theme => ({ boxShadow: 'none', textAlign: 'left', }, - accordionBody: { padding: '0' }, + accordionBody: { padding: '0', wordBreak: 'break-all' }, accordionActions: { padding: '0', justifyContent: 'flex-start', diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx index 67ccc1351d..150124bdf9 100644 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx +++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx @@ -95,6 +95,7 @@ export const StrategiesList = () => { ), + disableGlobalFilter: true, }, { Header: 'Name', @@ -147,6 +148,7 @@ export const StrategiesList = () => { ), width: 150, + disableGlobalFilter: true, disableSortBy: true, }, { @@ -155,6 +157,7 @@ export const StrategiesList = () => { }, { accessor: 'sortOrder', + disableGlobalFilter: true, sortType: 'number', }, ], diff --git a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx index fa3bd37999..5bd880b27d 100644 --- a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx +++ b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx @@ -71,6 +71,7 @@ export const TagTypeList = () => {