From 7ba9d2a577a335b068a250b1f9c878be917862b8 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 24 May 2022 10:58:06 +0200 Subject: [PATCH] Feat/new strategies table (#1012) * fix: add flex to toolbarcontainer * feat: add initial new table * feat: add styled badge * feat: remove dead code * fix: remove useContext import * fix: update context buttons to icon buttons * feat: add loading * fix: remove unused imports * Update src/component/strategies/StrategiesList/PredefinedBadge/PredefinedBadge.tsx Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> * fix: update spacing to use theme * fix: update loading * fix: update type Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> --- .../InstanceStatus/InstanceStatusBar.tsx | 2 + .../PermissionIconButton.tsx | 1 + .../Table/cells/LinkCell/LinkCell.styles.ts | 1 - .../common/Table/cells/LinkCell/LinkCell.tsx | 8 +- .../AddContextButton/AddContextButton.tsx | 45 ++- .../ContextActionsCell/ContextActionsCell.tsx | 1 + .../context/ContextList/ContextList.tsx | 16 +- .../feature/FeatureView/FeatureView.styles.ts | 1 + frontend/src/component/menu/routes.ts | 1 + .../AddStrategyButton/AddStrategyButton.tsx | 40 +++ .../PredefinedBadge/PredefinedBadge.tsx | 15 + .../StrategiesList/StrategiesList.styles.ts | 14 - .../StrategiesList/StrategiesList.test.tsx | 28 -- .../StrategiesList/StrategiesList.tsx | 308 ++++++++++++------ .../StrategiesList.test.tsx.snap | 270 --------------- frontend/src/themes/theme.ts | 1 + frontend/src/themes/themeTypes.ts | 5 + 17 files changed, 310 insertions(+), 447 deletions(-) create mode 100644 frontend/src/component/strategies/StrategiesList/AddStrategyButton/AddStrategyButton.tsx create mode 100644 frontend/src/component/strategies/StrategiesList/PredefinedBadge/PredefinedBadge.tsx delete mode 100644 frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts delete mode 100644 frontend/src/component/strategies/StrategiesList/StrategiesList.test.tsx delete mode 100644 frontend/src/component/strategies/StrategiesList/__snapshots__/StrategiesList.test.tsx.snap diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx index 61765baf8c..bbea3bdbaa 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.tsx @@ -72,6 +72,7 @@ const calculateTrialDaysRemaining = ( : undefined; }; +// TODO - Cleanup to use theme instead of colors const StyledBar = styled('aside')(({ theme }) => ({ position: 'relative', zIndex: 1, @@ -92,6 +93,7 @@ const StyledButton = styled(Button)(({ theme }) => ({ marginLeft: theme.spacing(2), })); +// TODO - Cleanup to use theme instead of colors const StyledInfoIcon = styled(Info)(({ theme }) => ({ color: colors.blue[500], })); diff --git a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx index a7ad231603..1f67327179 100644 --- a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx +++ b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx @@ -19,6 +19,7 @@ interface IPermissionIconButtonProps { type?: 'button'; edge?: IconButtonProps['edge']; tooltipProps?: Omit; + size?: string; } interface IButtonProps extends IPermissionIconButtonProps { 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 4e900c3329..2497c2f55c 100644 --- a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts +++ b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.styles.ts @@ -28,7 +28,6 @@ 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/LinkCell/LinkCell.tsx b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx index a446278839..1f607c6346 100644 --- a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx +++ b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx @@ -13,7 +13,12 @@ interface ILinkCellProps { subtitle?: string; } -export const LinkCell: FC = ({ title, to, subtitle }) => { +export const LinkCell: FC = ({ + title, + to, + subtitle, + children, +}) => { const { classes: styles } = useStyles(); const search = useSearchHighlightContext(); @@ -28,6 +33,7 @@ export const LinkCell: FC = ({ title, to, subtitle }) => { }} > {title} + {children} = () => { const smallScreen = useMediaQuery('(max-width:700px)'); - const { hasAccess } = useContext(AccessContext); const navigate = useNavigate(); return ( - navigate('/context/create')} - size="large" - > - - - - } - elseShow={ - - } - /> + navigate('/context/create')} + size="large" + tooltipProps={{ title: 'Add context type' }} + > + + + } + elseShow={ + navigate('/context/create')} + permission={CREATE_CONTEXT_FIELD} + color="primary" + variant="contained" + > + New context field + } /> ); diff --git a/frontend/src/component/context/ContextList/ContextActionsCell/ContextActionsCell.tsx b/frontend/src/component/context/ContextList/ContextActionsCell/ContextActionsCell.tsx index d589332a49..2b5fe2a74c 100644 --- a/frontend/src/component/context/ContextList/ContextActionsCell/ContextActionsCell.tsx +++ b/frontend/src/component/context/ContextList/ContextActionsCell/ContextActionsCell.tsx @@ -21,6 +21,7 @@ export const ContextActionsCell: VFC = ({ return ( { - const { hasAccess } = useContext(AccessContext); const [showDelDialogue, setShowDelDialogue] = useState(false); const [name, setName] = useState(); const { context, refetchUnleashContext, loading } = useUnleashContext(); const { removeContext } = useContextsApi(); const { setToastData, setToastApiError } = useToast(); + const data = useMemo(() => { if (loading) { return Array(5).fill({ @@ -57,6 +55,7 @@ const ContextList: VFC = () => { id: 'Icon', Cell: () => ( { }: any) => ( ), sortType: 'alphanumeric', @@ -118,7 +113,7 @@ const ContextList: VFC = () => { sortType: 'number', }, ], - [hasAccess] + [] ); const initialState = useMemo( @@ -172,6 +167,7 @@ const ContextList: VFC = () => { return ( ({ }, toolbarContainer: { flexShrink: 0, + display: 'flex', }, innerContainer: { padding: '1rem 2rem', diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 5a0e133ee2..e9057382b8 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -1,6 +1,7 @@ import { FeatureToggleListContainer } from 'component/feature/FeatureToggleList/FeatureToggleListContainer'; import { StrategyView } from 'component/strategies/StrategyView/StrategyView'; import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList'; + import { ArchiveListContainer } from 'component/archive/ArchiveListContainer'; import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList'; import { AddonList } from 'component/addons/AddonList/AddonList'; diff --git a/frontend/src/component/strategies/StrategiesList/AddStrategyButton/AddStrategyButton.tsx b/frontend/src/component/strategies/StrategiesList/AddStrategyButton/AddStrategyButton.tsx new file mode 100644 index 0000000000..91e7cc2d7c --- /dev/null +++ b/frontend/src/component/strategies/StrategiesList/AddStrategyButton/AddStrategyButton.tsx @@ -0,0 +1,40 @@ +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; +import PermissionButton from 'component/common/PermissionButton/PermissionButton'; +import { useMediaQuery } from '@mui/material'; +import { CREATE_STRATEGY } from 'component/providers/AccessProvider/permissions'; +import { ADD_NEW_STRATEGY_ID } from 'utils/testIds'; +import { Add } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; + +export const AddStrategyButton = () => { + const smallScreen = useMediaQuery('(max-width:700px)'); + const navigate = useNavigate(); + + return ( + navigate('/strategies/create')} + permission={CREATE_STRATEGY} + size="large" + tooltipProps={{ title: 'New strategy' }} + > + + + } + elseShow={ + navigate('/strategies/create')} + color="primary" + permission={CREATE_STRATEGY} + data-testid={ADD_NEW_STRATEGY_ID} + > + New strategy + + } + /> + ); +}; diff --git a/frontend/src/component/strategies/StrategiesList/PredefinedBadge/PredefinedBadge.tsx b/frontend/src/component/strategies/StrategiesList/PredefinedBadge/PredefinedBadge.tsx new file mode 100644 index 0000000000..b4505c7d84 --- /dev/null +++ b/frontend/src/component/strategies/StrategiesList/PredefinedBadge/PredefinedBadge.tsx @@ -0,0 +1,15 @@ +import { styled } from '@mui/material'; + +export const PredefinedBadge = () => { + return Predefined; +}; + +const StyledBadge = styled('div')(({ theme }) => ({ + padding: theme.spacing(0.3, 1.25), + backgroundColor: theme.palette.predefinedBadgeColor, + textDecoration: 'none', + color: theme.palette.text.primary, + display: 'inline-block', + borderRadius: theme.shape.borderRadius, + marginLeft: theme.spacing(1.5), +})); diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts b/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts deleted file mode 100644 index e0f73a17e4..0000000000 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.styles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - listItem: { - padding: '0', - ['& a']: { - textDecoration: 'none', - color: 'inherit', - }, - '&:hover': { - backgroundColor: theme.palette.grey[200], - }, - }, -})); diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.test.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.test.tsx deleted file mode 100644 index 093a0e6437..0000000000 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { MemoryRouter } from 'react-router-dom'; -import { StrategiesList } from './StrategiesList'; -import renderer from 'react-test-renderer'; -import { ADMIN } from 'component/providers/AccessProvider/permissions'; -import UIProvider from 'component/providers/UIProvider/UIProvider'; -import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider'; -import { ThemeProvider } from 'themes/ThemeProvider'; -import { AccessProviderMock } from 'component/providers/AccessProvider/AccessProviderMock'; - -test('renders correctly', () => { - const tree = renderer.create( - - - - - - - - - - - - ); - - expect(tree).toMatchSnapshot(); -}); diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx index 1b4f51626b..188a5fdfa4 100644 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx +++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx @@ -1,16 +1,7 @@ -import { useContext, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import useMediaQuery from '@mui/material/useMediaQuery'; +import { useState, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { IconButton, Tooltip, Box } from '@mui/material'; import { - IconButton, - List, - ListItem, - ListItemAvatar, - ListItemText, - Tooltip, -} from '@mui/material'; -import { - Add, Delete, Edit, Extension, @@ -18,26 +9,35 @@ import { VisibilityOff, } from '@mui/icons-material'; import { - CREATE_STRATEGY, DELETE_STRATEGY, UPDATE_STRATEGY, } from 'component/providers/AccessProvider/permissions'; +import { + Table, + SortableTableHeader, + TableBody, + TableCell, + TableRow, + TablePlaceholder, + TableSearch, +} from 'component/common/Table'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { useStyles } from './StrategiesList.styles'; -import AccessContext from 'contexts/AccessContext'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; -import { ADD_NEW_STRATEGY_ID } from 'utils/testIds'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; -import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { formatStrategyName } from 'utils/strategyNames'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { IStrategy } from 'interfaces/strategy'; - +import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; +import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { sortTypes } from 'utils/sortTypes'; +import { useTable, useGlobalFilter, useSortBy } from 'react-table'; +import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton'; +import { PredefinedBadge } from './PredefinedBadge/PredefinedBadge'; interface IDialogueMetaData { show: boolean; title: string; @@ -46,9 +46,6 @@ interface IDialogueMetaData { export const StrategiesList = () => { const navigate = useNavigate(); - const { classes: styles } = useStyles(); - const smallScreen = useMediaQuery('(max-width:700px)'); - const { hasAccess } = useContext(AccessContext); const [dialogueMetaData, setDialogueMetaData] = useState( { show: false, @@ -56,50 +53,137 @@ export const StrategiesList = () => { onConfirm: () => {}, } ); - const { strategies, refetchStrategies } = useStrategies(); + + const { strategies, refetchStrategies, loading } = useStrategies(); const { removeStrategy, deprecateStrategy, reactivateStrategy } = useStrategiesApi(); const { setToastData, setToastApiError } = useToast(); - const headerButton = () => ( - navigate('/strategies/create')} - permission={CREATE_STRATEGY} - tooltipProps={{ title: 'New strategy' }} + const data = useMemo(() => { + if (loading) { + return Array(5).fill({ + name: 'Context name', + description: 'Context description when loading', + }); + } + + return strategies.map( + ({ name, description, editable, deprecated }) => ({ + name, + description, + editable, + deprecated, + }) + ); + }, [strategies, loading]); + + const columns = useMemo( + () => [ + { + id: 'Icon', + Cell: () => ( + + + + ), + }, + { + Header: 'Name', + accessor: 'name', + width: '90%', + Cell: ({ + row: { + original: { name, description, deprecated, editable }, + }, + }: any) => { + const subTitleText = deprecated + ? `${description} (deprecated)` + : description; + return ( + - - - } - elseShow={ - navigate('/strategies/create')} - color="primary" - permission={CREATE_STRATEGY} - data-testid={ADD_NEW_STRATEGY_ID} - > - New strategy - - } - /> - } - /> + } + /> + + ); + }, + sortType: 'alphanumeric', + }, + { + Header: 'Actions', + id: 'Actions', + align: 'center', + Cell: ({ row: { original } }: any) => ( + + + {editButton(original)} + {deleteButton(original)} + + ), + width: 150, + disableSortBy: true, + }, + { + accessor: 'description', + disableSortBy: true, + }, + { + accessor: 'sortOrder', + sortType: 'number', + }, + ], + [] ); - const strategyLink = (name: string, deprecated: boolean) => ( - - {formatStrategyName(name)} - (Deprecated)} - /> - + const initialState = useMemo( + () => ({ + sortBy: [{ id: 'name', desc: false }], + hiddenColumns: ['description', 'sortOrder'], + }), + [] + ); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { globalFilter }, + setGlobalFilter, + } = useTable( + { + columns: columns as any[], // TODO: fix after `react-table` v8 update + data, + initialState, + sortTypes, + autoResetGlobalFilter: false, + autoResetSortBy: false, + disableSortRemove: true, + }, + useGlobalFilter, + useSortBy ); const onReactivateStrategy = (strategy: IStrategy) => { @@ -176,10 +260,7 @@ export const StrategiesList = () => { +
@@ -216,7 +297,7 @@ export const StrategiesList = () => { } elseShow={ - +
@@ -251,56 +332,85 @@ export const StrategiesList = () => { /> ); - const strategyList = () => - strategies.map(strategy => ( - - - - - - - - - - )); - const onDialogConfirm = () => { dialogueMetaData?.onConfirm(); - setDialogueMetaData(prev => ({ ...prev, show: false })); + setDialogueMetaData((prev: IDialogueMetaData) => ({ + ...prev, + show: false, + })); }; return ( } + isLoading={loading} + header={ + + + + + + } + /> + } > + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+ 0} + show={ + + No strategies found matching “ + {globalFilter} + ” + + } + elseShow={ + + No strategies available. Get started by adding + one. + + } + /> + } + /> + - setDialogueMetaData(prev => ({ ...prev, show: false })) + setDialogueMetaData((prev: IDialogueMetaData) => ({ + ...prev, + show: false, + })) } /> - - 0} - show={<>{strategyList()}} - elseShow={No strategies found} - /> -
); }; diff --git a/frontend/src/component/strategies/StrategiesList/__snapshots__/StrategiesList.test.tsx.snap b/frontend/src/component/strategies/StrategiesList/__snapshots__/StrategiesList.test.tsx.snap deleted file mode 100644 index b7dfab212c..0000000000 --- a/frontend/src/component/strategies/StrategiesList/__snapshots__/StrategiesList.test.tsx.snap +++ /dev/null @@ -1,270 +0,0 @@ -// Vitest Snapshot v1 - -exports[`renders correctly 1`] = ` -[ -
-
-
-
-
-
-

- Strategies -

-
-
- - - -
-
-
-
-
-
    -
  • -
    - - - -
    -
    - - - - Gradual rollout - - - -

    - Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit. -

    -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
  • -
-
-
-
, -
- Navigated to Strategies -
, -] -`; diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 76a4508849..fcb73430ab 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -81,6 +81,7 @@ export default createTheme({ highlight: '#FFEACC', sidebarContainer: 'rgba(32,32,33, 0.2)', grey: colors.grey, + predefinedBadgeColor: colors.green[100], text: { primary: colors.grey[900], secondary: colors.grey[800], diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index 0c59e9ea74..3ad1e1dd6b 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -49,6 +49,11 @@ declare module '@mui/material/styles' { abandoned: string; }; dividerAlternative: string; + + /** + * Color for predefined badge + */ + predefinedBadgeColor: string; /** * For table header hover effect. */