From f690fe86da6eee0f544990bdd4b795acc4476ebc Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:42:35 +0100 Subject: [PATCH] feat: new search for feature toggle list table (#5454) Filtering of feature toggles list with backend --- .../common/FilterItem/FilterItem.tsx | 45 +- .../src/component/common/Search/Search.tsx | 4 +- .../FeatureToggleFilters.tsx | 41 ++ .../FeatureToggleListTable.tsx | 208 ++++----- .../LegacyFeatureToggleListTable.tsx | 414 ++++++++++++++++++ frontend/src/component/menu/Header/Header.tsx | 11 +- .../__snapshots__/routes.test.tsx.snap | 10 + frontend/src/component/menu/routes.ts | 9 + .../useFeatureSearch/useFeatureSearch.ts | 1 - 9 files changed, 616 insertions(+), 127 deletions(-) create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx create mode 100644 frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx diff --git a/frontend/src/component/common/FilterItem/FilterItem.tsx b/frontend/src/component/common/FilterItem/FilterItem.tsx index 100fa07336..598d8366dd 100644 --- a/frontend/src/component/common/FilterItem/FilterItem.tsx +++ b/frontend/src/component/common/FilterItem/FilterItem.tsx @@ -13,12 +13,17 @@ import { FilterItemChip } from './FilterItemChip/FilterItemChip'; interface IFilterItemProps { label: string; options: Array<{ label: string; value: string }>; + onChange?: (value: string) => void; } const singularOperators = ['IS', 'IS_NOT']; -const pluralOperators = ['IS_IN', 'IS_NOT_IN']; +const pluralOperators = ['IS_ANY_OF', 'IS_NOT_ANY_OF']; -export const FilterItem: FC = ({ label, options }) => { +export const FilterItem: FC = ({ + label, + options, + onChange, +}) => { const ref = useRef(null); const [selectedOptions, setSelectedOptions] = useState([]); const [anchorEl, setAnchorEl] = useState(null); @@ -35,8 +40,28 @@ export const FilterItem: FC = ({ label, options }) => { setAnchorEl(null); }; + const handleOnChange = ( + op: typeof operator, + values: typeof selectedOptions, + ) => { + const value = values.length + ? `${op}:${values?.map((option) => option.value).join(', ')}` + : ''; + onChange?.(value); + }; + + const handleOperatorChange = (value: string) => { + setOperator(value); + handleOnChange(value, selectedOptions); + }; + + const handleOptionsChange = (values: typeof selectedOptions) => { + setSelectedOptions(values); + handleOnChange(operator, values); + }; + const onDelete = () => { - setSelectedOptions([]); + handleOptionsChange([]); onClose(); }; @@ -46,19 +71,19 @@ export const FilterItem: FC = ({ label, options }) => { (selectedOption) => selectedOption.value === value, ) ) { - setSelectedOptions( - selectedOptions?.filter( - (selectedOption) => selectedOption.value !== value, - ), + const newOptions = selectedOptions?.filter( + (selectedOption) => selectedOption.value !== value, ); + handleOptionsChange(newOptions); } else { - setSelectedOptions([ + const newOptions = [ ...(selectedOptions ?? []), options.find((option) => option.value === value) ?? { label: '', value: '', }, - ]); + ]; + handleOptionsChange(newOptions); } }; @@ -80,7 +105,7 @@ export const FilterItem: FC = ({ label, options }) => { onClick={onClick} operator={operator} operatorOptions={currentOperators} - onChangeOperator={setOperator} + onChangeOperator={handleOperatorChange} /> { diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx new file mode 100644 index 0000000000..d8cd4862ae --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -0,0 +1,41 @@ +import { VFC } from 'react'; +import { Box } from '@mui/material'; +import { FilterItem } from 'component/common/FilterItem/FilterItem'; +import useProjects from 'hooks/api/getters/useProjects/useProjects'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useTableState } from 'hooks/useTableState'; + +export type FeatureTogglesListFilters = { + projectId?: string; +}; + +interface IFeatureToggleFiltersProps { + state: FeatureTogglesListFilters; + onChange: (value: FeatureTogglesListFilters) => void; +} + +export const FeatureToggleFilters: VFC = ({ + state, + onChange, +}) => { + const { projects } = useProjects(); + const projectsOptions = (projects || []).map((project) => ({ + label: project.name, + value: project.id, + })); + + return ( + ({ marginBottom: theme.spacing(2) })}> + 1} + show={() => ( + onChange({ projectId: value })} + /> + )} + /> + + ); +}; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 401f4eafda..6a884b2d97 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -1,37 +1,30 @@ import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; import { - Box, 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 { Link as RouterLink } from 'react-router-dom'; +import { useFlexLayout, usePagination, 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 { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; 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'; @@ -40,8 +33,20 @@ 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'; -import { FilterItem } from 'component/common/FilterItem/FilterItem'; -import { useUiFlag } from 'hooks/useUiFlag'; +import { sortTypes } from 'utils/sortTypes'; +import { + FeatureToggleFilters, + FeatureTogglesListFilters, +} from './FeatureToggleFilters/FeatureToggleFilters'; +import { + DEFAULT_PAGE_LIMIT, + useFeatureSearch, +} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; +import { + defaultQueryKeys, + defaultStoredKeys, + useTableState, +} from 'hooks/useTableState'; export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ name: 'Name of the feature', @@ -55,12 +60,15 @@ 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, -); +type FeatureToggleListState = { + page: string; + pageSize: string; + sortBy?: string; + sortOrder?: string; + projectId?: string; + search?: string; + favorites?: string; +} & FeatureTogglesListFilters; export const FeatureToggleListTable: VFC = () => { const theme = useTheme(); @@ -71,36 +79,54 @@ export const FeatureToggleListTable: VFC = () => { 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 featureSearchFrontend = useUiFlag('featureSearchFrontend'); + const [tableState, setTableState] = useTableState( + { + page: '1', + pageSize: `${DEFAULT_PAGE_LIMIT}`, + sortBy: 'createdAt', + sortOrder: 'desc', + projectId: '', + search: '', + favorites: 'true', + }, + 'featureToggleList', + [...defaultQueryKeys, 'projectId'], + [...defaultStoredKeys, 'projectId'], + ); + const offset = (Number(tableState.page) - 1) * Number(tableState?.pageSize); + const { + features = [], + loading, + refetch: refetchFeatures, + } = useFeatureSearch( + offset, + Number(tableState.pageSize), + { + sortBy: tableState.sortBy || 'createdAt', + sortOrder: tableState.sortOrder || 'desc', + favoritesFirst: tableState.favorites === 'true', + }, + tableState.projectId || undefined, + tableState.search || '', + ); const [initialState] = useState(() => ({ sortBy: [ { - id: searchParams.get('sort') || storedParams.id, - desc: searchParams.has('order') - ? searchParams.get('order') === 'desc' - : storedParams.desc, + id: tableState.sortBy || 'createdAt', + desc: tableState.sortOrder === 'desc', }, ], hiddenColumns: ['description'], - globalFilter: searchParams.get('search') || '', + pageSize: Number(tableState.pageSize), + pageIndex: Number(tableState.page) - 1, })); - 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) => { + // FIXME: projectId is missing try { if (feature?.favorite) { await unfavorite(feature.project, feature.name); @@ -122,8 +148,15 @@ export const FeatureToggleListTable: VFC = () => { { Header: ( + setTableState({ + favorites: + tableState.favorites === 'true' + ? 'false' + : 'true', + }) + } /> ), accessor: 'favorite', @@ -194,38 +227,22 @@ export const FeatureToggleListTable: VFC = () => { 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], + [tableState.favorites], ); - const { - data: searchedData, - getSearchText, - getSearchContext, - } = useSearch(columns, searchValue, features); - const data = useMemo( () => - searchedData?.length === 0 && loading - ? featuresPlaceholder - : searchedData, - [searchedData, loading], + features?.length === 0 && loading ? featuresPlaceholder : features, + [features, loading], ); const { headerGroups, rows, prepareRow, - state: { sortBy }, + state: { pageIndex, pageSize, sortBy }, setHiddenColumns, } = useTable( { @@ -237,11 +254,23 @@ export const FeatureToggleListTable: VFC = () => { autoResetSortBy: false, disableSortRemove: true, disableMultiSort: true, + manualSortBy: true, + manualPagination: true, }, useSortBy, useFlexLayout, + usePagination, ); + useEffect(() => { + setTableState({ + page: `${pageIndex + 1}`, + pageSize: `${pageSize}`, + sortBy: sortBy[0]?.id || 'createdAt', + sortOrder: sortBy[0]?.desc ? 'desc' : 'asc', + }); + }, [pageIndex, pageSize, sortBy]); + useConditionallyHiddenColumns( [ { @@ -260,32 +289,7 @@ export const FeatureToggleListTable: VFC = () => { 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]); + const setSearchValue = (search = '') => setTableState({ search }); if (!(environments.length > 0)) { return null; @@ -308,12 +312,10 @@ export const FeatureToggleListTable: VFC = () => { show={ <> @@ -361,38 +363,16 @@ export const FeatureToggleListTable: VFC = () => { condition={isSmallScreen} show={ } /> } > - {featureSearchFrontend && ( - ({ marginBottom: theme.spacing(2) })}> - - - )} - + + { condition={rows.length === 0} show={ 0} + condition={(tableState.search || '')?.length > 0} show={ No feature toggles found matching “ - {searchValue} + {tableState.search} ” } diff --git a/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx new file mode 100644 index 0000000000..ef966da974 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/LegacyFeatureToggleListTable.tsx @@ -0,0 +1,414 @@ +import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; +import { + Box, + 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 db01fa51c3..c34afabc79 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -115,6 +115,7 @@ const StyledIconButton = styled(IconButton)<{ })); const Header: VFC = () => { + const featureSearchFrontend = useUiFlag('featureSearchFrontend'); const { onSetThemeMode, themeMode } = useThemeMode(); const theme = useTheme(); const adminId = useId(); @@ -191,7 +192,15 @@ const Header: VFC = () => { Projects - Feature toggles + + Feature toggles + 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 47e03ef5e1..b044e6e477 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -123,6 +123,16 @@ exports[`returns all baseRoutes 1`] = ` "title": "Feature toggles", "type": "protected", }, + { + "component": [Function], + "flag": "featureSearchFrontend", + "menu": { + "mobile": true, + }, + "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 0f930b829d..a643714226 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -1,4 +1,5 @@ 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'; @@ -144,9 +145,17 @@ 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: { mobile: true }, + flag: 'featureSearchFrontend', }, // Playground diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts index 37a77623cd..c09872592c 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -128,7 +128,6 @@ const getFeatureSearchFetcher = ( const searchQueryParams = translateToQueryParams(searchValue); const sortQueryParams = translateToSortQueryParams(sortingRules); const project = projectId ? `projectId=${projectId}&` : ''; - const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`; const fetcher = () => { const path = formatApiPath(KEY);