From 203d6ac8483b55a12ca3027e46ae64b3b147d3aa Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:37:49 +0100 Subject: [PATCH] refactor: paginated project table cleanup (#5646) - added `getRowId` - fix row selection - move and rename paginated table --- .../FeatureToggleListTable.test.tsx | 16 +- .../FeatureToggleListTable.tsx | 12 +- frontend/src/component/menu/Header/Header.tsx | 9 +- .../__snapshots__/routes.test.tsx.snap | 8 - frontend/src/component/menu/routes.ts | 11 +- .../ExperimentalProjectFeatures.tsx | 93 --- .../ExperimentalColumnsMenu.styles.ts | 0 .../ExperimentalColumnsMenu.tsx | 0 .../FeatureToggleCell/FeatureToggleCell.tsx | 4 +- .../PaginatedProjectFeatureToggles.tsx} | 24 +- .../ProjectFeatureTogglesHeader.tsx | 0 .../ProjectOverviewFilters.tsx | 0 .../TablePlaceholder/TablePlaceholder.tsx | 0 .../hooks/useDefaultColumnVisibility.ts | 0 .../hooks/useRowActions.tsx | 0 .../src/component/project/Project/Project.tsx | 5 - .../PaginatedProjectFeatureToggles.tsx | 751 ------------------ .../project/Project/ProjectOverview.tsx | 11 +- 18 files changed, 41 insertions(+), 903 deletions(-) delete mode 100644 frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/ExperimentalColumnsMenu/ExperimentalColumnsMenu.styles.ts (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/FeatureToggleCell/FeatureToggleCell.tsx (91%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx => PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx} (95%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/ProjectOverviewFilters.tsx (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/TablePlaceholder/TablePlaceholder.tsx (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/hooks/useDefaultColumnVisibility.ts (100%) rename frontend/src/component/project/Project/{ExperimentalProjectFeatures/ExperimentalProjectTable => PaginatedProjectFeatureToggles}/hooks/useRowActions.tsx (100%) delete mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx index 4643326dac..b50f1c3bda 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx @@ -34,7 +34,11 @@ const setupNoFeaturesReturned = () => }); const setupApi = (features: APIFeature[], projects: APIProject[]) => { - testServerRoute(server, '/api/admin/ui-config', {}); + testServerRoute(server, '/api/admin/ui-config', { + flags: { + featureSearchFrontend: true, + }, + }); testServerRoute(server, '/api/admin/projects', { projects, @@ -75,12 +79,12 @@ const setupApi = (features: APIFeature[], projects: APIProject[]) => { }); }; -const verifyTableFeature = async (feature: UIFeature) => { +const verifyTableFeature = async (feature: Partial) => { await screen.findByText('Feature toggles'); await screen.findByText('Add Filter'); - Object.values(feature).forEach((value) => { - expect(screen.getByText(value)).toBeInTheDocument(); - }); + await Promise.all( + Object.values(feature).map((value) => screen.findByText(value)), + ); }; const filterFeaturesByProject = async (projectName: string) => { @@ -137,6 +141,6 @@ test('Filter table by project', async () => { 'No feature toggles available. Get started by adding a new feature toggle.', ); expect(window.location.href).toContain( - '?offset=0&columns=&project=IS%3Aproject-b', + '?sort=createdAt&order=desc&offset=0&columns=&project=IS%3Aproject-b', ); }); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index c5d4549475..91f63320ed 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -51,6 +51,8 @@ import { withTableState } from 'utils/withTableState'; import { usePersistentTableState } from 'hooks/usePersistentTableState'; import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable'; export const featuresPlaceholder = Array(15).fill({ name: 'Name of the feature', @@ -62,7 +64,7 @@ export const featuresPlaceholder = Array(15).fill({ const columnHelper = createColumnHelper(); -export const FeatureToggleListTable: VFC = () => { +const FeatureToggleListTableComponent: VFC = () => { const theme = useTheme(); const { environments } = useEnvironments(); const enabledEnvironments = environments @@ -379,3 +381,11 @@ export const FeatureToggleListTable: VFC = () => { ); }; + +export const FeatureToggleListTable: VFC = () => { + const featureSearchFrontend = useUiFlag('featureSearchFrontend'); + + if (featureSearchFrontend) return ; + + return ; +}; diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index 29ac970f51..dd584c5df4 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -115,7 +115,6 @@ const StyledIconButton = styled(IconButton)<{ })); const Header: VFC = () => { - const featureSearchFrontend = useUiFlag('featureSearchFrontend'); const { onSetThemeMode, themeMode } = useThemeMode(); const theme = useTheme(); const adminId = useId(); @@ -192,13 +191,7 @@ const Header: VFC = () => { Projects - + Feature toggles Playground diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index e22b08f7cd..47e03ef5e1 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -123,14 +123,6 @@ exports[`returns all baseRoutes 1`] = ` "title": "Feature toggles", "type": "protected", }, - { - "component": [Function], - "flag": "featureSearchFrontend", - "menu": {}, - "path": "/features-new", - "title": "Feature toggles", - "type": "protected", - }, { "component": { "$$typeof": Symbol(react.lazy), diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index c26e343c02..0f930b829d 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -1,5 +1,4 @@ import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable'; -import { FeatureToggleListTable as LegacyFeatureToggleListTable } from 'component/feature/FeatureToggleList/LegacyFeatureToggleListTable'; import { StrategyView } from 'component/strategies/StrategyView/StrategyView'; import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList'; import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList'; @@ -145,17 +144,9 @@ export const routes: IRoute[] = [ { path: '/features', title: 'Feature toggles', - component: LegacyFeatureToggleListTable, - type: 'protected', - menu: { mobile: true }, - }, - { - path: '/features-new', - title: 'Feature toggles', component: FeatureToggleListTable, type: 'protected', - menu: {}, // TODO: Add mobile menu when removing `featureSearchFrontend` flag - flag: 'featureSearchFrontend', + menu: { mobile: true }, }, // Playground diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx deleted file mode 100644 index f07211159a..0000000000 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useEffect } from 'react'; -import useProject, { - useProjectNameOrId, -} from 'hooks/api/getters/useProject/useProject'; -import { Box, styled } from '@mui/material'; -import { ProjectFeatureToggles } from '../ProjectFeatureToggles/ProjectFeatureToggles'; -import { usePageTitle } from 'hooks/usePageTitle'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { useLastViewedProject } from 'hooks/useLastViewedProject'; - -import { useUiFlag } from 'hooks/useUiFlag'; -import { ExperimentalProjectFeatureToggles } from './ExperimentalProjectTable/ExperimentalProjectTable'; - -const refreshInterval = 15 * 1000; - -const StyledContainer = styled('div')(({ theme }) => ({ - display: 'flex', - [theme.breakpoints.down('md')]: { - flexDirection: 'column', - }, -})); - -const StyledProjectToggles = styled('div')(() => ({ - width: '100%', - minWidth: 0, -})); - -const StyledContentContainer = styled(Box)(() => ({ - display: 'flex', - flexDirection: 'column', - width: '100%', - minWidth: 0, -})); - -const PaginatedProjectOverview = () => { - const projectId = useRequiredPathParam('projectId'); - const { project, loading: projectLoading } = useProject(projectId, { - refreshInterval, - }); - - const { environments } = project; - - return ( - - - - - - - - ); -}; - -/** - * @deprecated remove when flag `featureSearchFrontend` is removed - */ -export const ExperimentalProjectFeatures = () => { - const projectId = useRequiredPathParam('projectId'); - const projectName = useProjectNameOrId(projectId); - const { project, loading, refetch } = useProject(projectId, { - refreshInterval, - }); - const { features, environments } = project; - usePageTitle(`Project overview – ${projectName}`); - const { setLastViewed } = useLastViewedProject(); - const featureSearchFrontend = useUiFlag('featureSearchFrontend'); - - useEffect(() => { - setLastViewed(projectId); - }, [projectId, setLastViewed]); - - if (featureSearchFrontend) return ; - - return ( - - - - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalColumnsMenu/ExperimentalColumnsMenu.styles.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ExperimentalColumnsMenu/ExperimentalColumnsMenu.styles.ts similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalColumnsMenu/ExperimentalColumnsMenu.styles.ts rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ExperimentalColumnsMenu/ExperimentalColumnsMenu.styles.ts diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/FeatureToggleCell/FeatureToggleCell.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/FeatureToggleCell/FeatureToggleCell.tsx similarity index 91% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/FeatureToggleCell/FeatureToggleCell.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/FeatureToggleCell/FeatureToggleCell.tsx index 73592e688c..e165758891 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/FeatureToggleCell/FeatureToggleCell.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/FeatureToggleCell/FeatureToggleCell.tsx @@ -3,8 +3,8 @@ import { styled } from '@mui/material'; import { flexRow } from 'themes/themeStyles'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; -import { FeatureToggleSwitch } from '../../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch'; -import type { UseFeatureToggleSwitchType } from '../../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types'; +import { FeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch'; +import type { UseFeatureToggleSwitchType } from '../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types'; import { type FeatureEnvironmentSchema } from 'openapi'; const StyledSwitchContainer = styled('div', { diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx similarity index 95% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx index 832ccd1060..b754acf12f 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; @@ -9,17 +9,17 @@ import { PaginatedTable } from 'component/common/Table'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; -import { ProjectEnvironmentType } from '../../ProjectFeatureToggles/hooks/useEnvironmentsRef'; -import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell'; +import { ProjectEnvironmentType } from '../ProjectFeatureToggles/hooks/useEnvironmentsRef'; +import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell'; import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; -import { MemoizedRowSelectCell } from '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell'; +import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell'; import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; -import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; +import { ProjectFeaturesBatchActions } from '../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; -import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; +import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; import useLoading from 'hooks/useLoading'; import { DEFAULT_PAGE_LIMIT, @@ -32,11 +32,11 @@ import { FilterItemParam, } from 'utils/serializeQueryParams'; import { - ArrayParam, - encodeQueryParams, NumberParam, StringParam, + ArrayParam, withDefault, + encodeQueryParams, } from 'use-query-params'; import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; @@ -50,7 +50,7 @@ import { Placeholder } from './TablePlaceholder/TablePlaceholder'; import { useRowActions } from './hooks/useRowActions'; import { useUiFlag } from 'hooks/useUiFlag'; -interface IExperimentalProjectFeatureTogglesProps { +interface IPaginatedProjectFeatureTogglesProps { environments: IProject['environments']; refreshInterval?: number; storageKey?: string; @@ -60,12 +60,13 @@ const formatEnvironmentColumnId = (environment: string) => `environment:${environment}`; const columnHelper = createColumnHelper(); +const getRowId = (row: { name: string }) => row.name; -export const ExperimentalProjectFeatureToggles = ({ +export const PaginatedProjectFeatureToggles = ({ environments, refreshInterval = 15 * 1000, storageKey = 'project-feature-toggles', -}: IExperimentalProjectFeatureTogglesProps) => { +}: IPaginatedProjectFeatureTogglesProps) => { const projectId = useRequiredPathParam('projectId'); const featuresExportImport = useUiFlag('featuresExportImport'); @@ -338,6 +339,7 @@ export const ExperimentalProjectFeatureToggles = ({ state: { columnVisibility: defaultColumnVisibility, }, + getRowId, }), ); diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/TablePlaceholder/TablePlaceholder.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/TablePlaceholder/TablePlaceholder.tsx similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/TablePlaceholder/TablePlaceholder.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/TablePlaceholder/TablePlaceholder.tsx diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/hooks/useDefaultColumnVisibility.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/hooks/useDefaultColumnVisibility.ts rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useDefaultColumnVisibility.ts diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/hooks/useRowActions.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useRowActions.tsx similarity index 100% rename from frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/hooks/useRowActions.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/hooks/useRowActions.tsx diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 500e00c896..45529fc76f 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -40,7 +40,6 @@ import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadg import { Badge } from 'component/common/Badge/Badge'; import { ProjectDoraMetrics } from './ProjectDoraMetrics/ProjectDoraMetrics'; import { UiFlags } from 'interfaces/uiConfig'; -import { ExperimentalProjectFeatures } from './ExperimentalProjectFeatures/ExperimentalProjectFeatures'; import { HiddenProjectIconWithTooltip } from './HiddenProjectIconWithTooltip/HiddenProjectIconWithTooltip'; const StyledBadge = styled(Badge)(({ theme }) => ({ @@ -288,10 +287,6 @@ export const Project = () => { } /> } /> } /> - } - /> } diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx deleted file mode 100644 index 8fcfbd90c1..0000000000 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx +++ /dev/null @@ -1,751 +0,0 @@ -import React, { - type CSSProperties, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import { - Checkbox, - IconButton, - styled, - Tooltip, - useMediaQuery, - Box, - useTheme, -} from '@mui/material'; -import { Add } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; -import { - useFlexLayout, - usePagination, - useRowSelect, - useSortBy, - useTable, -} from 'react-table'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { PageContent } from 'component/common/PageContent/PageContent'; -import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; -import { getCreateTogglePath } from 'utils/routePathHelpers'; -import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; -import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; -import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; -import { IProject } from 'interfaces/project'; -import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; -import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; -import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; -import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; -import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch'; -import { Search } from 'component/common/Search/Search'; -import { IFeatureToggleListItem } from 'interfaces/featureToggle'; -import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; -import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; -import { ProjectEnvironmentType } from './hooks/useEnvironmentsRef'; -import { ActionsCell } from './ActionsCell/ActionsCell'; -import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu'; -import { useStyles } from './ProjectFeatureToggles.styles'; -import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; -import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; -import FileDownload from '@mui/icons-material/FileDownload'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; -import { MemoizedRowSelectCell } from './RowSelectCell/RowSelectCell'; -import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; -import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; -import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; -import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; -import { ListItemType } from './ProjectFeatureToggles.types'; -import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell'; -import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch'; -import useLoading from 'hooks/useLoading'; -import { StickyPaginationBar } from '../../../common/Table/StickyPaginationBar/StickyPaginationBar'; -import { - DEFAULT_PAGE_LIMIT, - useFeatureSearch, -} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; -import mapValues from 'lodash.mapvalues'; -import { usePersistentTableState } from 'hooks/usePersistentTableState'; -import { BooleansStringParam } from 'utils/serializeQueryParams'; -import { - NumberParam, - StringParam, - ArrayParam, - withDefault, -} from 'use-query-params'; - -const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ - whiteSpace: 'nowrap', -})); - -interface IPaginatedProjectFeatureTogglesProps { - environments: IProject['environments']; - style?: CSSProperties; - refreshInterval?: number; - storageKey?: string; -} - -const staticColumns = ['Select', 'Actions', 'name', 'favorite']; - -export const PaginatedProjectFeatureToggles = ({ - environments, - style, - refreshInterval = 15 * 1000, - storageKey = 'project-feature-toggles', -}: IPaginatedProjectFeatureTogglesProps) => { - const projectId = useRequiredPathParam('projectId'); - const [tableState, setTableState] = usePersistentTableState( - `${storageKey}-${projectId}`, - { - offset: withDefault(NumberParam, 0), - limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), - query: StringParam, - favoritesFirst: withDefault(BooleansStringParam, true), - sortBy: withDefault(StringParam, 'createdAt'), - sortOrder: withDefault(StringParam, 'desc'), - columns: ArrayParam, - }, - ); - - const { features, total, refetch, loading, initialLoad } = useFeatureSearch( - mapValues({ ...tableState, project: `IS:${projectId}` }, (value) => - value ? `${value}` : undefined, - ), - { - refreshInterval, - }, - ); - const onChange = refetch; - - const { classes: styles } = useStyles(); - const bodyLoadingRef = useLoading(loading); - const headerLoadingRef = useLoading(initialLoad); - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{ - featureId?: string; - stale?: boolean; - }>({}); - const [featureArchiveState, setFeatureArchiveState] = useState< - string | undefined - >(); - const [isCustomColumns, setIsCustomColumns] = useState( - Boolean(tableState.columns), - ); - const { onToggle: onFeatureToggle, modals: featureToggleModals } = - useFeatureToggleSwitch(projectId); - - const navigate = useNavigate(); - const { favorite, unfavorite } = useFavoriteFeaturesApi(); - const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); - const [showExportDialog, setShowExportDialog] = useState(false); - const { uiConfig } = useUiConfig(); - - const onFavorite = useCallback( - async (feature: IFeatureToggleListItem) => { - if (feature?.favorite) { - await unfavorite(projectId, feature.name); - } else { - await favorite(projectId, feature.name); - } - onChange(); - }, - [projectId, onChange], - ); - - const showTagsColumn = useMemo( - () => features.some((feature) => feature?.tags?.length), - [features], - ); - - const columns = useMemo( - () => [ - { - id: 'Select', - Header: ({ getToggleAllRowsSelectedProps }: any) => ( - - ), - Cell: ({ row }: any) => ( - - ), - maxWidth: 50, - disableSortBy: true, - hideInMenu: true, - styles: { - borderRadius: 0, - }, - }, - { - id: 'favorite', - Header: ( - - setTableState({ - favoritesFirst: !tableState.favoritesFirst, - }) - } - /> - ), - accessor: 'favorite', - Cell: ({ row: { original: feature } }: any) => ( - onFavorite(feature)} - /> - ), - maxWidth: 50, - disableSortBy: true, - hideInMenu: true, - }, - { - Header: 'Seen', - accessor: 'lastSeenAt', - Cell: ({ value, row: { original: feature } }: any) => { - return ( - - ); - }, - align: 'center', - maxWidth: 80, - }, - { - Header: 'Type', - accessor: 'type', - Cell: FeatureTypeCell, - align: 'center', - filterName: 'type', - maxWidth: 80, - }, - { - Header: 'Name', - accessor: 'name', - Cell: ({ - value, - }: { - value: string; - }) => ( - - - - - - ), - minWidth: 100, - sortType: 'alphanumeric', - searchable: true, - }, - ...(showTagsColumn - ? [ - { - id: 'tags', - Header: 'Tags', - accessor: (row: IFeatureToggleListItem) => - row.tags - ?.map(({ type, value }) => `${type}:${value}`) - .join('\n') || '', - Cell: FeatureTagCell, - width: 80, - searchable: true, - filterName: 'tags', - filterBy( - row: IFeatureToggleListItem, - values: string[], - ) { - return includesFilter( - getColumnValues(this, row), - values, - ); - }, - }, - ] - : []), - { - Header: 'Created', - accessor: 'createdAt', - Cell: DateCell, - minWidth: 120, - }, - ...environments.map( - (projectEnvironment: ProjectEnvironmentType | string) => { - const name = - typeof projectEnvironment === 'string' - ? projectEnvironment - : (projectEnvironment as ProjectEnvironmentType) - .environment; - const isChangeRequestEnabled = - isChangeRequestConfigured(name); - const FeatureToggleCell = createFeatureToggleCell( - projectId, - name, - isChangeRequestEnabled, - onChange, - onFeatureToggle, - ); - - return { - Header: loading ? () => '' : name, - maxWidth: 90, - id: `environment:${name}`, - accessor: (row: ListItemType) => { - return row.environments?.[name]?.enabled; - }, - align: 'center', - Cell: FeatureToggleCell, - sortType: 'boolean', - sortDescFirst: true, - filterName: name, - filterParsing: (value: boolean) => - value ? 'enabled' : 'disabled', - }; - }, - ), - { - id: 'Actions', - maxWidth: 56, - width: 56, - Cell: (props: { - row: { - original: ListItemType; - }; - }) => ( - - ), - disableSortBy: true, - hideInMenu: true, - styles: { - borderRadius: 0, - }, - }, - ], - [projectId, environments, loading, tableState.favoritesFirst, onChange], - ); - - const [showTitle, setShowTitle] = useState(true); - - const featuresData = useMemo( - () => - features.map((feature) => ({ - ...feature, - environments: Object.fromEntries( - environments.map((env) => { - const thisEnv = feature?.environments?.find( - (featureEnvironment) => - featureEnvironment?.name === env.environment, - ); - return [ - env.environment, - { - name: env.environment, - enabled: thisEnv?.enabled || false, - variantCount: thisEnv?.variantCount || 0, - lastSeenAt: thisEnv?.lastSeenAt, - type: thisEnv?.type, - hasStrategies: thisEnv?.hasStrategies, - hasEnabledStrategies: - thisEnv?.hasEnabledStrategies, - }, - ]; - }), - ), - someEnabledEnvironmentHasVariants: - feature.environments?.some( - (featureEnvironment) => - featureEnvironment.variantCount && - featureEnvironment.variantCount > 0 && - featureEnvironment.enabled, - ) || false, - })), - [features, environments], - ); - - const { getSearchText, getSearchContext } = useSearch( - columns, - tableState.query || '', - featuresData, - ); - - const allColumnIds = columns - .map( - (column: any) => - (column?.id as string) || - (typeof column?.accessor === 'string' - ? (column?.accessor as string) - : ''), - ) - .filter(Boolean); - - const initialState = useMemo( - () => ({ - sortBy: [ - { - id: tableState.sortBy || 'createdAt', - desc: tableState.sortOrder === 'desc', - }, - ], - ...(tableState.columns - ? { - hiddenColumns: allColumnIds.filter( - (id) => - !tableState.columns?.includes(id) && - !staticColumns.includes(id), - ), - } - : {}), - pageSize: tableState.limit, - pageIndex: tableState.offset * tableState.limit, - selectedRowIds: {}, - }), - [initialLoad], - ); - - const data = useMemo(() => { - if (initialLoad || loading) { - const loadingData = Array(tableState.limit) - .fill(null) - .map((_, index) => ({ - id: index, // Assuming `id` is a required property - type: '-', - name: `Feature name ${index}`, - createdAt: new Date().toISOString(), - environments: [ - { - name: 'production', - enabled: false, - }, - ], - })); - // Coerce loading data to FeatureSchema[] - return loadingData as unknown as typeof featuresData; - } - return featuresData; - }, [loading, featuresData]); - - const pageCount = useMemo( - () => Math.ceil((total || 0) / tableState.limit), - [total, tableState.limit], - ); - const getRowId = useCallback((row: any) => row.name, []); - - const { - allColumns, - headerGroups, - rows, - state: { pageIndex, pageSize, hiddenColumns, selectedRowIds, sortBy }, - canNextPage, - canPreviousPage, - previousPage, - nextPage, - setPageSize, - prepareRow, - setHiddenColumns, - toggleAllRowsSelected, - } = useTable( - { - columns: columns as any[], // TODO: fix after `react-table` v8 update - data, - initialState, - autoResetHiddenColumns: false, - autoResetSelectedRows: false, - disableSortRemove: true, - autoResetSortBy: false, - manualSortBy: true, - manualPagination: true, - pageCount, - getRowId, - }, - useFlexLayout, - useSortBy, - usePagination, - useRowSelect, - ); - - // Refetching - https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/faq.md#how-can-i-use-the-table-state-to-fetch-new-data - useEffect(() => { - setTableState({ - offset: pageIndex * pageSize, - limit: pageSize, - sortBy: sortBy[0]?.id || 'createdAt', - sortOrder: sortBy[0]?.desc ? 'desc' : 'asc', - }); - }, [pageIndex, pageSize, sortBy]); - - useEffect(() => { - // FIXME: refactor column visibility logic when switching to react-table v8 - if (!loading && isCustomColumns) { - setTableState({ - columns: - hiddenColumns !== undefined - ? allColumnIds.filter( - (id) => - !hiddenColumns.includes(id) && - !staticColumns.includes(id), - ) - : undefined, - }); - } - }, [loading, isCustomColumns, hiddenColumns]); - - const showPaginationBar = Boolean(total && total > pageSize); - const paginatedStyles = showPaginationBar - ? { - borderBottomLeftRadius: 0, - borderBottomRightRadius: 0, - } - : {}; - - return ( - <> - ({ - padding: `${theme.spacing(2.5)} ${theme.spacing( - 3.125, - )}`, - })} - > - - { - setTableState({ - query: value, - }); - }} - onFocus={() => - setShowTitle(false) - } - onBlur={() => - setShowTitle(true) - } - hasFilters - getSearchContext={ - getSearchContext - } - id='projectFeatureToggles' - /> - } - /> - - setIsCustomColumns(true) - } - /> - - - - setShowExportDialog( - true, - ) - } - sx={(theme) => ({ - marginRight: - theme.spacing(2), - })} - > - - - - } - /> - - navigate( - getCreateTogglePath(projectId), - ) - } - maxWidth='960px' - Icon={Add} - projectId={projectId} - permission={CREATE_FEATURE} - data-testid='NAVIGATE_TO_CREATE_FEATURE' - > - New feature toggle - - - } - > - { - setTableState({ query: value }); - }} - hasFilters - getSearchContext={getSearchContext} - id='projectFeatureToggles' - /> - } - /> - - - } - > -
- - - - - 0} - show={ - - - No feature toggles found matching - “ - {tableState.query} - ” - - - } - elseShow={ - - - No feature toggles available. Get - started by adding a new feature - toggle. - - - } - /> - } - /> - { - setFeatureStaleDialogState({}); - onChange(); - }} - featureId={featureStaleDialogState.featureId || ''} - projectId={projectId} - /> - { - setFeatureArchiveState(undefined); - }} - featureIds={[featureArchiveState || '']} - projectId={projectId} - /> - setShowExportDialog(false)} - environments={environments.map( - ({ environment }) => environment, - )} - /> - } - /> - {featureToggleModals} -
-
- - } - /> - - toggleAllRowsSelected(false)} - /> - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index d44bea44db..9b767a0392 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -10,10 +10,9 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useLastViewedProject } from 'hooks/useLastViewedProject'; import { ProjectStats } from './ProjectStats/ProjectStats'; import { useUiFlag } from 'hooks/useUiFlag'; -import { PaginatedProjectFeatureToggles } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles'; - +import { PaginatedProjectFeatureToggles } from './PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles'; import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; -import { FeatureTypeCount } from '../../../interfaces/project'; +import { type FeatureTypeCount } from '../../../interfaces/project'; const refreshInterval = 15 * 1000; @@ -39,9 +38,8 @@ const StyledContentContainer = styled(Box)(({ theme }) => ({ })); const PaginatedProjectOverview: FC<{ - fullWidth?: boolean; storageKey?: string; -}> = ({ fullWidth, storageKey = 'project-overview' }) => { +}> = ({ storageKey = 'project-overview' }) => { const projectId = useRequiredPathParam('projectId'); const { project } = useProjectOverview(projectId, { refreshInterval, @@ -70,9 +68,6 @@ const PaginatedProjectOverview: FC<{