diff --git a/frontend/cypress/integration/projects/overview.spec.ts b/frontend/cypress/integration/projects/overview.spec.ts index 20dc503128..bf0551f007 100644 --- a/frontend/cypress/integration/projects/overview.spec.ts +++ b/frontend/cypress/integration/projects/overview.spec.ts @@ -14,8 +14,7 @@ describe('project overview', () => { const featureToggleName = `${featureTogglePrefix}-${randomId}`; const projectName = `unleash-e2e-project-overview-${randomId}`; const baseUrl = Cypress.config().baseUrl; - const selectAll = - '[title="Toggle All Rows Selected"] input[type="checkbox"]'; + const selectAll = '[title="Select all rows"] input[type="checkbox"]'; before(() => { cy.runBefore(); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx index de08ae46ad..b3f2d15269 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.test.tsx @@ -35,12 +35,6 @@ const setupNoFeaturesReturned = () => }); const setupApi = (features: APIFeature[], projects: APIProject[]) => { - testServerRoute(server, '/api/admin/ui-config', { - flags: { - featureSearchFrontend: true, - }, - }); - testServerRoute(server, '/api/admin/projects', { projects, }); @@ -146,6 +140,6 @@ test('Filter table by project', async () => { 'No feature toggles found matching your criteria. Get started by adding a new feature toggle.', ); expect(window.location.href).toContain( - '?sort=createdAt&order=desc&offset=0&columns=&project=IS%3Aproject-b', + '?offset=0&columns=&project=IS%3Aproject-b', ); }, 10000); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index b7bb020369..f7c94583f9 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -50,7 +50,6 @@ 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'; import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions'; import useLoading from 'hooks/useLoading'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; @@ -68,7 +67,7 @@ export const featuresPlaceholder = Array(15).fill({ const columnHelper = createColumnHelper(); const feedbackCategory = 'search'; -const FeatureToggleListTableComponent: VFC = () => { +export const FeatureToggleListTable: VFC = () => { const theme = useTheme(); const { openFeedback } = useFeedback(feedbackCategory, 'automatic'); const { trackEvent } = usePlausibleTracker(); @@ -428,11 +427,3 @@ const FeatureToggleListTableComponent: VFC = () => { ); }; - -export const FeatureToggleListTable: VFC = () => { - const featureSearchFrontend = useUiFlag('featureSearchFrontend'); - - if (featureSearchFrontend) return ; - - return ; -}; diff --git a/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx deleted file mode 100644 index 5806b83136..0000000000 --- a/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; -import { - IconButton, - Link, - Tooltip, - useMediaQuery, - useTheme, -} from '@mui/material'; -import { Link as RouterLink, useSearchParams } from 'react-router-dom'; -import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; -import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; -import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; -import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; -import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; -import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; -import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { PageContent } from 'component/common/PageContent/PageContent'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { createLocalStorage } from 'utils/createLocalStorage'; -import { FeatureSchema } from 'openapi'; -import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'; -import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; -import { useSearch } from 'hooks/useSearch'; -import { Search } from 'component/common/Search/Search'; -import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; -import { usePinnedFavorites } from 'hooks/usePinnedFavorites'; -import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; -import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; -import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; -import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; -import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; -import FileDownload from '@mui/icons-material/FileDownload'; -import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; -import { ExportDialog } from './ExportDialog'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { focusable } from 'themes/themeStyles'; -import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; -import useToast from 'hooks/useToast'; - -export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ - name: 'Name of the feature', - description: 'Short description of the feature', - type: '-', - createdAt: new Date(2022, 1, 1), - project: 'projectID', -}); - -export type PageQueryType = Partial< - Record<'sort' | 'order' | 'search' | 'favorites', string> ->; - -const defaultSort: SortingRule = { id: 'createdAt', desc: true }; - -const { value: storedParams, setValue: setStoredParams } = createLocalStorage( - 'FeatureToggleListTable:v1', - defaultSort, -); - -/** - * @deprecated remove with flag `featureSearchFrontend` - */ -export const FeatureToggleListTable: VFC = () => { - const theme = useTheme(); - const { environments } = useEnvironments(); - const enabledEnvironments = environments - .filter((env) => env.enabled) - .map((env) => env.name); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); - const [showExportDialog, setShowExportDialog] = useState(false); - const { features = [], loading, refetchFeatures } = useFeatures(); - const [searchParams, setSearchParams] = useSearchParams(); - const { setToastApiError } = useToast(); - const { uiConfig } = useUiConfig(); - - const [initialState] = useState(() => ({ - sortBy: [ - { - id: searchParams.get('sort') || storedParams.id, - desc: searchParams.has('order') - ? searchParams.get('order') === 'desc' - : storedParams.desc, - }, - ], - hiddenColumns: ['description'], - globalFilter: searchParams.get('search') || '', - })); - const { value: globalStore, setValue: setGlobalStore } = - useGlobalLocalStorage(); - const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } = - usePinnedFavorites( - searchParams.has('favorites') - ? searchParams.get('favorites') === 'true' - : globalStore.favorites, - ); - const [searchValue, setSearchValue] = useState(initialState.globalFilter); - const { favorite, unfavorite } = useFavoriteFeaturesApi(); - const onFavorite = useCallback( - async (feature: any) => { - try { - if (feature?.favorite) { - await unfavorite(feature.project, feature.name); - } else { - await favorite(feature.project, feature.name); - } - refetchFeatures(); - } catch (error) { - setToastApiError( - 'Something went wrong, could not update favorite', - ); - } - }, - [favorite, refetchFeatures, unfavorite, setToastApiError], - ); - - const columns = useMemo( - () => [ - { - Header: ( - - ), - accessor: 'favorite', - Cell: ({ row: { original: feature } }: any) => ( - onFavorite(feature)} - /> - ), - maxWidth: 50, - disableSortBy: true, - }, - { - Header: 'Seen', - accessor: 'lastSeenAt', - Cell: ({ value, row: { original: feature } }: any) => { - return ; - }, - align: 'center', - maxWidth: 80, - }, - { - Header: 'Type', - accessor: 'type', - Cell: FeatureTypeCell, - align: 'center', - maxWidth: 85, - }, - { - Header: 'Name', - accessor: 'name', - minWidth: 150, - Cell: FeatureNameCell, - sortType: 'alphanumeric', - searchable: true, - }, - { - id: 'tags', - Header: 'Tags', - accessor: (row: FeatureSchema) => - row.tags - ?.map(({ type, value }) => `${type}:${value}`) - .join('\n') || '', - Cell: FeatureTagCell, - width: 80, - searchable: true, - }, - { - Header: 'Created', - accessor: 'createdAt', - Cell: DateCell, - maxWidth: 150, - }, - { - Header: 'Project ID', - accessor: 'project', - Cell: ({ value }: { value: string }) => ( - - ), - sortType: 'alphanumeric', - maxWidth: 150, - filterName: 'project', - searchable: true, - }, - { - Header: 'State', - accessor: 'stale', - Cell: FeatureStaleCell, - sortType: 'boolean', - maxWidth: 120, - filterName: 'state', - filterParsing: (value: any) => (value ? 'stale' : 'active'), - }, - // Always hidden -- for search - { - accessor: 'description', - Header: 'Description', - searchable: true, - }, - ], - [isFavoritesPinned], - ); - - const { - data: searchedData, - getSearchText, - getSearchContext, - } = useSearch(columns, searchValue, features); - - const data = useMemo( - () => - searchedData?.length === 0 && loading - ? featuresPlaceholder - : searchedData, - [searchedData, loading], - ); - - const { - headerGroups, - rows, - prepareRow, - state: { sortBy }, - setHiddenColumns, - } = useTable( - { - columns: columns as any[], - data, - initialState, - sortTypes, - autoResetHiddenColumns: false, - autoResetSortBy: false, - disableSortRemove: true, - disableMultiSort: true, - }, - useSortBy, - useFlexLayout, - ); - - useConditionallyHiddenColumns( - [ - { - condition: !features.some(({ tags }) => tags?.length), - columns: ['tags'], - }, - { - condition: isSmallScreen, - columns: ['type', 'createdAt', 'tags'], - }, - { - condition: isMediumScreen, - columns: ['lastSeenAt', 'stale'], - }, - ], - setHiddenColumns, - columns, - ); - - useEffect(() => { - const tableState: PageQueryType = {}; - tableState.sort = sortBy[0].id; - if (sortBy[0].desc) { - tableState.order = 'desc'; - } - if (searchValue) { - tableState.search = searchValue; - } - if (isFavoritesPinned) { - tableState.favorites = 'true'; - } - - setSearchParams(tableState, { - replace: true, - }); - setStoredParams({ - id: sortBy[0].id, - desc: sortBy[0].desc || false, - }); - setGlobalStore((params) => ({ - ...params, - favorites: Boolean(isFavoritesPinned), - })); - }, [sortBy, searchValue, setSearchParams, isFavoritesPinned]); - - if (!(environments.length > 0)) { - return null; - } - - return ( - - - - - - } - /> - - View archive - - - - setShowExportDialog(true) - } - sx={(theme) => ({ - marginRight: theme.spacing(2), - })} - > - - - - } - /> - - - - } - > - - } - /> - - } - > - - - - 0} - show={ - - No feature toggles found matching “ - {searchValue} - ” - - } - elseShow={ - - No feature toggles available. Get started by - adding a new feature toggle. - - } - /> - } - /> - setShowExportDialog(false)} - environments={enabledEnvironments} - /> - } - /> - - ); -}; diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index 6eff96bc6c..5d4ade9f08 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -157,7 +157,6 @@ const Header: VFC = () => { const [configRef, setConfigRef] = useState(null); const disableNotifications = useUiFlag('disableNotifications'); - const hasSearch = useUiFlag('featureSearchFrontend'); const { uiConfig, isOss } = useUiConfig(); const smallScreen = useMediaQuery(theme.breakpoints.down('md')); const [openDrawer, setOpenDrawer] = useState(false); @@ -249,17 +248,7 @@ const Header: VFC = () => { Projects - Search - } - elseShow={ - - Feature toggles - - } - /> + Search Playground setConfigRef(e.currentTarget)} 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 d2821aabe8..6110b83b6a 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -119,17 +119,6 @@ exports[`returns all baseRoutes 1`] = ` "menu": { "mobile": true, }, - "notFlag": "featureSearchFrontend", - "path": "/features", - "title": "Feature toggles", - "type": "protected", - }, - { - "component": [Function], - "flag": "featureSearchFrontend", - "menu": { - "mobile": true, - }, "path": "/search", "title": "Search", "type": "protected", diff --git a/frontend/src/component/menu/__tests__/routes.test.tsx b/frontend/src/component/menu/__tests__/routes.test.tsx index f0d7f11330..a456924b9c 100644 --- a/frontend/src/component/menu/__tests__/routes.test.tsx +++ b/frontend/src/component/menu/__tests__/routes.test.tsx @@ -5,6 +5,6 @@ test('returns all baseRoutes', () => { }); test('getRoute() returns named route', () => { - const featuresRoute = getRoute('/features'); - expect(featuresRoute?.path).toEqual('/features'); + const featuresRoute = getRoute('/search'); + expect(featuresRoute?.path).toEqual('/search'); }); diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 26be4509c5..3315f8aaaf 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -142,21 +142,12 @@ export const routes: IRoute[] = [ type: 'protected', menu: {}, }, - { - path: '/features', - title: 'Feature toggles', - component: FeatureToggleListTable, - type: 'protected', - menu: { mobile: true }, - notFlag: 'featureSearchFrontend', - }, { path: '/search', title: 'Search', component: FeatureToggleListTable, type: 'protected', menu: { mobile: true }, - flag: 'featureSearchFrontend', }, // Playground diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx similarity index 99% rename from frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index 8ce636f7d0..1abbb92efb 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -64,7 +64,7 @@ const formatEnvironmentColumnId = (environment: string) => const columnHelper = createColumnHelper(); const getRowId = (row: { name: string }) => row.name; -export const PaginatedProjectFeatureToggles = ({ +export const ProjectFeatureToggles = ({ environments, refreshInterval = 15 * 1000, storageKey = 'project-feature-toggles-v2', diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.types.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.types.ts similarity index 100% rename from frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.types.ts rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.types.ts diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx index 0df836335e..3a07eb9f5e 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx @@ -4,8 +4,8 @@ import { flexRow } from 'themes/themeStyles'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; import { FeatureToggleSwitch } from './FeatureToggleSwitch'; -import type { ListItemType } from '../ProjectFeatureToggles.types'; import type { UseFeatureToggleSwitchType } from './FeatureToggleSwitch.types'; +import { ListItemType } from '../../PaginatedProjectFeatureToggles/ProjectFeatureToggles.types'; const StyledSwitchContainer = styled('div', { shouldForwardProp: (prop) => prop !== 'hasWarning', diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx deleted file mode 100644 index 661943344f..0000000000 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ /dev/null @@ -1,700 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - Checkbox, - IconButton, - styled, - Tooltip, - useMediaQuery, - useTheme, - Box, -} from '@mui/material'; -import { Add } from '@mui/icons-material'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { - SortingRule, - useFlexLayout, - useRowSelect, - useSortBy, - useTable, -} from 'react-table'; -import type { FeatureSchema } from 'openapi'; -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 { 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 { createLocalStorage } from 'utils/createLocalStorage'; -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, - useEnvironmentsRef, -} from './hooks/useEnvironmentsRef'; -import { ActionsCell } from './ActionsCell/ActionsCell'; -import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu'; -import { useStyles } from './ProjectFeatureToggles.styles'; -import { usePinnedFavorites } from 'hooks/usePinnedFavorites'; -import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; -import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; -import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; -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 '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar'; -import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; -import { MemoizedFeatureEnvironmentSeenCell } from '../../../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 useToast from 'hooks/useToast'; - -const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ - whiteSpace: 'nowrap', -})); - -interface IProjectFeatureTogglesProps { - features: IProject['features']; - environments: IProject['environments']; - loading: boolean; - onChange: () => void; - total?: number; - style?: React.CSSProperties; -} - -const staticColumns = ['Select', 'Actions', 'name', 'favorite']; - -const defaultSort: SortingRule & { - columns?: string[]; -} = { id: 'createdAt', desc: true }; - -/** - * @deprecated remove when flag `featureSearchFrontend` is removed - */ -export const ProjectFeatureToggles = ({ - features, - loading, - environments: newEnvironments = [], - onChange, - total, - style = {}, -}: IProjectFeatureTogglesProps) => { - const { classes: styles } = useStyles(); - const theme = useTheme(); - const { setToastApiError } = useToast(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{ - featureId?: string; - stale?: boolean; - }>({}); - const [featureArchiveState, setFeatureArchiveState] = useState< - string | undefined - >(); - const projectId = useRequiredPathParam('projectId'); - const { onToggle: onFeatureToggle, modals: featureToggleModals } = - useFeatureToggleSwitch(projectId); - - const { value: storedParams, setValue: setStoredParams } = - createLocalStorage( - `${projectId}:FeatureToggleListTable:v1`, - defaultSort, - ); - const { value: globalStore, setValue: setGlobalStore } = - useGlobalLocalStorage(); - const navigate = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); - const environments = useEnvironmentsRef( - loading - ? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }] - : newEnvironments, - ); - const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } = - usePinnedFavorites( - searchParams.has('favorites') - ? searchParams.get('favorites') === 'true' - : globalStore.favorites, - ); - const { favorite, unfavorite } = useFavoriteFeaturesApi(); - const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); - const [showExportDialog, setShowExportDialog] = useState(false); - const { uiConfig } = useUiConfig(); - - const onFavorite = useCallback( - async (feature: IFeatureToggleListItem) => { - try { - if (feature?.favorite) { - await unfavorite(projectId, feature.name); - } else { - await favorite(projectId, feature.name); - } - onChange(); - } catch (error) { - setToastApiError( - 'Something went wrong, could not update favorite', - ); - } - }, - [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: ( - - ), - 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((value: ProjectEnvironmentType | string) => { - const name = - typeof value === 'string' - ? value - : (value as ProjectEnvironmentType).environment; - const isChangeRequestEnabled = isChangeRequestConfigured(name); - const FeatureToggleCell = createFeatureToggleCell( - projectId, - name, - isChangeRequestEnabled, - onChange, - onFeatureToggle, - ); - - return { - Header: loading ? () => '' : name, - maxWidth: 90, - id: `environments.${name}`, - accessor: (row: ListItemType) => - row.environments[name]?.enabled, - align: 'center', - Cell: FeatureToggleCell, - sortType: 'boolean', - 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], - ); - - const [searchValue, setSearchValue] = useState( - searchParams.get('search') || '', - ); - - 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, - ); - return [ - env, - { - name: env, - 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 > 0 && - featureEnvironment.enabled, - ) || false, - })), - [features, environments], - ); - - const { - data: searchedData, - getSearchText, - getSearchContext, - } = useSearch(columns, searchValue, featuresData); - - const data = useMemo(() => { - if (loading) { - return Array(6).fill({ - type: '-', - name: 'Feature name', - createdAt: new Date(), - environments: { - production: { name: 'production', enabled: false }, - }, - }) as FeatureSchema[]; - } - return searchedData; - }, [loading, searchedData]); - - const initialState = useMemo( - () => { - const allColumnIds = columns - .map( - (column: any) => - (column?.id as string) || - (typeof column?.accessor === 'string' - ? (column?.accessor as string) - : ''), - ) - .filter(Boolean); - let hiddenColumns = environments - .filter((_, index) => index >= 3) - .map((environment) => `environment:${environment}`); - - if (searchParams.has('columns')) { - const columnsInParams = - searchParams.get('columns')?.split(',') || []; - const visibleColumns = [...staticColumns, ...columnsInParams]; - hiddenColumns = allColumnIds.filter( - (columnId) => !visibleColumns.includes(columnId), - ); - } else if (storedParams.columns) { - const visibleColumns = [ - ...staticColumns, - ...storedParams.columns, - ]; - hiddenColumns = allColumnIds.filter( - (columnId) => !visibleColumns.includes(columnId), - ); - } - - return { - sortBy: [ - { - id: - searchParams.get('sort') || - storedParams.id || - 'createdAt', - desc: searchParams.has('order') - ? searchParams.get('order') === 'desc' - : storedParams.desc, - }, - ], - hiddenColumns, - selectedRowIds: {}, - }; - }, - [environments], // eslint-disable-line react-hooks/exhaustive-deps - ); - - const getRowId = useCallback((row: any) => row.name, []); - const { - allColumns, - headerGroups, - rows, - state: { selectedRowIds, sortBy, hiddenColumns }, - prepareRow, - setHiddenColumns, - toggleAllRowsSelected, - } = useTable( - { - columns: columns as any[], // TODO: fix after `react-table` v8 update - data, - initialState, - sortTypes, - autoResetHiddenColumns: false, - autoResetSelectedRows: false, - disableSortRemove: true, - autoResetSortBy: false, - getRowId, - }, - useFlexLayout, - useSortBy, - useRowSelect, - ); - - useEffect(() => { - if (loading) { - return; - } - const tableState: Record = {}; - tableState.sort = sortBy[0].id; - if (sortBy[0].desc) { - tableState.order = 'desc'; - } - if (searchValue) { - tableState.search = searchValue; - } - if (isFavoritesPinned) { - tableState.favorites = 'true'; - } - tableState.columns = allColumns - .map(({ id }) => id) - .filter( - (id) => - !staticColumns.includes(id) && !hiddenColumns?.includes(id), - ) - .join(','); - - setSearchParams(tableState, { - replace: true, - }); - setStoredParams((params) => ({ - ...params, - id: sortBy[0].id, - desc: sortBy[0].desc || false, - columns: tableState.columns.split(','), - })); - setGlobalStore((params) => ({ - ...params, - favorites: Boolean(isFavoritesPinned), - })); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - loading, - sortBy, - hiddenColumns, - searchValue, - setSearchParams, - isFavoritesPinned, - ]); - - return ( - <> - ({ - padding: `${theme.spacing(2.5)} ${theme.spacing( - 3.125, - )}`, - })} - > - - - setShowTitle(false) - } - onBlur={() => - setShowTitle(true) - } - hasFilters - getSearchContext={ - getSearchContext - } - id='projectFeatureToggles' - /> - } - /> - - - - - 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 - - - } - > - - } - /> - - - } - > - - - - 0} - show={ - - - No feature toggles found matching - “ - {searchValue} - ” - - - } - elseShow={ - - - No feature toggles found matching your - criteria. 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} - /> - } - /> - {featureToggleModals} - - - toggleAllRowsSelected(false)} - onChange={onChange} - /> - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index 1b78820794..9857543ebd 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -1,18 +1,14 @@ import { FC, useEffect } from 'react'; -import useProject, { - useProjectNameOrId, -} from 'hooks/api/getters/useProject/useProject'; import { Box, styled } from '@mui/material'; -import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles'; import ProjectInfo from './ProjectInfo/ProjectInfo'; -import { usePageTitle } from 'hooks/usePageTitle'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { useLastViewedProject } from 'hooks/useLastViewedProject'; import { ProjectStats } from './ProjectStats/ProjectStats'; -import { useUiFlag } from 'hooks/useUiFlag'; -import { PaginatedProjectFeatureToggles } from './PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles'; -import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; -import { type FeatureTypeCount } from '../../../interfaces/project'; +import { ProjectFeatureToggles } from './PaginatedProjectFeatureToggles/ProjectFeatureToggles'; +import useProjectOverview, { + useProjectOverviewNameOrId, +} from 'hooks/api/getters/useProjectOverview/useProjectOverview'; +import { usePageTitle } from 'hooks/usePageTitle'; +import { useLastViewedProject } from 'hooks/useLastViewedProject'; const refreshInterval = 15 * 1000; @@ -37,13 +33,19 @@ const StyledContentContainer = styled(Box)(({ theme }) => ({ minWidth: 0, })); -const PaginatedProjectOverview: FC<{ +const ProjectOverview: FC<{ storageKey?: string; }> = ({ storageKey = 'project-overview-v2' }) => { const projectId = useRequiredPathParam('projectId'); + const projectName = useProjectOverviewNameOrId(projectId); const { project } = useProjectOverview(projectId, { refreshInterval, }); + usePageTitle(`Project overview – ${projectName}`); + const { setLastViewed } = useLastViewedProject(); + useEffect(() => { + setLastViewed(projectId); + }, [projectId, setLastViewed]); const { members, @@ -67,7 +69,7 @@ const PaginatedProjectOverview: FC<{ - { - const projectId = useRequiredPathParam('projectId'); - const projectName = useProjectNameOrId(projectId); - const { project, loading, refetch } = useProject(projectId, { - refreshInterval, - }); - const { members, features, health, description, environments, stats } = - project; - usePageTitle(`Project overview – ${projectName}`); - const { setLastViewed } = useLastViewedProject(); - const featureSearchFrontend = useUiFlag('featureSearchFrontend'); - - useEffect(() => { - setLastViewed(projectId); - }, [projectId, setLastViewed]); - - if (featureSearchFrontend) return ; - - const featureTypeCounts = features.reduce( - (acc: FeatureTypeCount[], feature) => { - const existingEntry = acc.find( - (entry) => entry.type === feature.type, - ); - if (existingEntry) { - existingEntry.count += 1; - } else { - acc.push({ type: feature.type, count: 1 }); - } - return acc; - }, - [], - ); - - return ( - - - - - - - - - - ); -}; - export default ProjectOverview; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 4e008270ba..d696dd3658 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -65,7 +65,6 @@ export type UiFlags = { dependentFeatures?: boolean; scheduledConfigurationChanges?: boolean; featureSearchAPI?: boolean; - featureSearchFrontend?: boolean; newStrategyConfiguration?: boolean; incomingWebhooks?: boolean; automatedActions?: boolean; diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 7ea030febc..f819e3f03f 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -95,10 +95,9 @@ exports[`should create default config 1`] = ` "executiveDashboard": false, "extendedUsageMetrics": false, "extendedUsageMetricsUI": false, - "featureSearchAPI": false, + "featureSearchAPI": true, "featureSearchFeedback": false, "featureSearchFeedbackPosting": false, - "featureSearchFrontend": false, "featuresExportImport": true, "feedbackComments": { "enabled": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index d00990d142..a84182cbf9 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -28,7 +28,6 @@ export type IFlagKey = | 'customRootRolesKillSwitch' | 'disableMetrics' | 'featureSearchAPI' - | 'featureSearchFrontend' | 'scheduledConfigurationChanges' | 'detectSegmentUsageInChangeRequests' | 'stripClientHeadersOn304' @@ -133,11 +132,7 @@ const flags: IFlags = { ), featureSearchAPI: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API, - false, - ), - featureSearchFrontend: parseEnvVarBoolean( - process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FRONTEND, - false, + true, ), scheduledConfigurationChanges: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_SCHEDULED_CONFIGURATION_CHANGES, diff --git a/src/server-dev.ts b/src/server-dev.ts index 9625c4b9d9..f9972953f2 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -41,7 +41,6 @@ process.nextTick(async () => { anonymiseEventLog: false, responseTimeWithAppNameKillSwitch: false, featureSearchAPI: true, - featureSearchFrontend: true, stripClientHeadersOn304: true, newStrategyConfiguration: true, stripHeadersOnAPI: true,