mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: new search for feature toggle list table (#5454)
Filtering of feature toggles list with backend
This commit is contained in:
		
							parent
							
								
									bb03253681
								
							
						
					
					
						commit
						f690fe86da
					
				| @ -13,12 +13,17 @@ import { FilterItemChip } from './FilterItemChip/FilterItemChip'; | |||||||
| interface IFilterItemProps { | interface IFilterItemProps { | ||||||
|     label: string; |     label: string; | ||||||
|     options: Array<{ label: string; value: string }>; |     options: Array<{ label: string; value: string }>; | ||||||
|  |     onChange?: (value: string) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const singularOperators = ['IS', 'IS_NOT']; | 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<IFilterItemProps> = ({ label, options }) => { | export const FilterItem: FC<IFilterItemProps> = ({ | ||||||
|  |     label, | ||||||
|  |     options, | ||||||
|  |     onChange, | ||||||
|  | }) => { | ||||||
|     const ref = useRef<HTMLDivElement>(null); |     const ref = useRef<HTMLDivElement>(null); | ||||||
|     const [selectedOptions, setSelectedOptions] = useState<typeof options>([]); |     const [selectedOptions, setSelectedOptions] = useState<typeof options>([]); | ||||||
|     const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null); |     const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null); | ||||||
| @ -35,8 +40,28 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => { | |||||||
|         setAnchorEl(null); |         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 = () => { |     const onDelete = () => { | ||||||
|         setSelectedOptions([]); |         handleOptionsChange([]); | ||||||
|         onClose(); |         onClose(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -46,19 +71,19 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => { | |||||||
|                 (selectedOption) => selectedOption.value === value, |                 (selectedOption) => selectedOption.value === value, | ||||||
|             ) |             ) | ||||||
|         ) { |         ) { | ||||||
|             setSelectedOptions( |             const newOptions = selectedOptions?.filter( | ||||||
|                 selectedOptions?.filter( |  | ||||||
|                 (selectedOption) => selectedOption.value !== value, |                 (selectedOption) => selectedOption.value !== value, | ||||||
|                 ), |  | ||||||
|             ); |             ); | ||||||
|  |             handleOptionsChange(newOptions); | ||||||
|         } else { |         } else { | ||||||
|             setSelectedOptions([ |             const newOptions = [ | ||||||
|                 ...(selectedOptions ?? []), |                 ...(selectedOptions ?? []), | ||||||
|                 options.find((option) => option.value === value) ?? { |                 options.find((option) => option.value === value) ?? { | ||||||
|                     label: '', |                     label: '', | ||||||
|                     value: '', |                     value: '', | ||||||
|                 }, |                 }, | ||||||
|             ]); |             ]; | ||||||
|  |             handleOptionsChange(newOptions); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -80,7 +105,7 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => { | |||||||
|                     onClick={onClick} |                     onClick={onClick} | ||||||
|                     operator={operator} |                     operator={operator} | ||||||
|                     operatorOptions={currentOperators} |                     operatorOptions={currentOperators} | ||||||
|                     onChangeOperator={setOperator} |                     onChangeOperator={handleOperatorChange} | ||||||
|                 /> |                 /> | ||||||
|             </Box> |             </Box> | ||||||
|             <StyledPopover |             <StyledPopover | ||||||
|  | |||||||
| @ -176,7 +176,9 @@ export const Search = ({ | |||||||
|             </StyledSearch> |             </StyledSearch> | ||||||
| 
 | 
 | ||||||
|             <ConditionallyRender |             <ConditionallyRender | ||||||
|                 condition={Boolean(hasFilters) && showSuggestions} |                 condition={ | ||||||
|  |                     Boolean(hasFilters && getSearchContext) && showSuggestions | ||||||
|  |                 } | ||||||
|                 show={ |                 show={ | ||||||
|                     <SearchSuggestions |                     <SearchSuggestions | ||||||
|                         onSuggestion={(suggestion) => { |                         onSuggestion={(suggestion) => { | ||||||
|  | |||||||
| @ -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<IFeatureToggleFiltersProps> = ({ | ||||||
|  |     state, | ||||||
|  |     onChange, | ||||||
|  | }) => { | ||||||
|  |     const { projects } = useProjects(); | ||||||
|  |     const projectsOptions = (projects || []).map((project) => ({ | ||||||
|  |         label: project.name, | ||||||
|  |         value: project.id, | ||||||
|  |     })); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={projectsOptions.length > 1} | ||||||
|  |                 show={() => ( | ||||||
|  |                     <FilterItem | ||||||
|  |                         label='Project' | ||||||
|  |                         options={projectsOptions} | ||||||
|  |                         onChange={(value) => onChange({ projectId: value })} | ||||||
|  |                     /> | ||||||
|  |                 )} | ||||||
|  |             /> | ||||||
|  |         </Box> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -1,37 +1,30 @@ | |||||||
| import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; | import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; | ||||||
| import { | import { | ||||||
|     Box, |  | ||||||
|     IconButton, |     IconButton, | ||||||
|     Link, |     Link, | ||||||
|     Tooltip, |     Tooltip, | ||||||
|     useMediaQuery, |     useMediaQuery, | ||||||
|     useTheme, |     useTheme, | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import { Link as RouterLink, useSearchParams } from 'react-router-dom'; | import { Link as RouterLink } from 'react-router-dom'; | ||||||
| import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; | import { useFlexLayout, usePagination, useSortBy, useTable } from 'react-table'; | ||||||
| import { TablePlaceholder, VirtualizedTable } from 'component/common/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 { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||||
| import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | ||||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | 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 { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; | ||||||
| import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; | import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import { createLocalStorage } from 'utils/createLocalStorage'; |  | ||||||
| import { FeatureSchema } from 'openapi'; | import { FeatureSchema } from 'openapi'; | ||||||
| import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'; | import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'; | ||||||
| import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; | import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; | ||||||
| import { useSearch } from 'hooks/useSearch'; |  | ||||||
| import { Search } from 'component/common/Search/Search'; | import { Search } from 'component/common/Search/Search'; | ||||||
| import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; | import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; | ||||||
| import { usePinnedFavorites } from 'hooks/usePinnedFavorites'; |  | ||||||
| import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; | import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; | ||||||
| import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; | import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; | ||||||
| import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; | import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; | ||||||
| import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; |  | ||||||
| import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; | import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; | ||||||
| import FileDownload from '@mui/icons-material/FileDownload'; | import FileDownload from '@mui/icons-material/FileDownload'; | ||||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | 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 { focusable } from 'themes/themeStyles'; | ||||||
| import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; | import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; | ||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { FilterItem } from 'component/common/FilterItem/FilterItem'; | import { sortTypes } from 'utils/sortTypes'; | ||||||
| import { useUiFlag } from 'hooks/useUiFlag'; | 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({ | export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ | ||||||
|     name: 'Name of the feature', |     name: 'Name of the feature', | ||||||
| @ -55,12 +60,15 @@ export type PageQueryType = Partial< | |||||||
|     Record<'sort' | 'order' | 'search' | 'favorites', string> |     Record<'sort' | 'order' | 'search' | 'favorites', string> | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true }; | type FeatureToggleListState = { | ||||||
| 
 |     page: string; | ||||||
| const { value: storedParams, setValue: setStoredParams } = createLocalStorage( |     pageSize: string; | ||||||
|     'FeatureToggleListTable:v1', |     sortBy?: string; | ||||||
|     defaultSort, |     sortOrder?: string; | ||||||
| ); |     projectId?: string; | ||||||
|  |     search?: string; | ||||||
|  |     favorites?: string; | ||||||
|  | } & FeatureTogglesListFilters; | ||||||
| 
 | 
 | ||||||
| export const FeatureToggleListTable: VFC = () => { | export const FeatureToggleListTable: VFC = () => { | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
| @ -71,36 +79,54 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); |     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||||
|     const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); |     const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); | ||||||
|     const [showExportDialog, setShowExportDialog] = useState(false); |     const [showExportDialog, setShowExportDialog] = useState(false); | ||||||
|     const { features = [], loading, refetchFeatures } = useFeatures(); | 
 | ||||||
|     const [searchParams, setSearchParams] = useSearchParams(); |  | ||||||
|     const { setToastApiError } = useToast(); |     const { setToastApiError } = useToast(); | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
| 
 |     const [tableState, setTableState] = useTableState<FeatureToggleListState>( | ||||||
|     const featureSearchFrontend = useUiFlag('featureSearchFrontend'); |         { | ||||||
|  |             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(() => ({ |     const [initialState] = useState(() => ({ | ||||||
|         sortBy: [ |         sortBy: [ | ||||||
|             { |             { | ||||||
|                 id: searchParams.get('sort') || storedParams.id, |                 id: tableState.sortBy || 'createdAt', | ||||||
|                 desc: searchParams.has('order') |                 desc: tableState.sortOrder === 'desc', | ||||||
|                     ? searchParams.get('order') === 'desc' |  | ||||||
|                     : storedParams.desc, |  | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|         hiddenColumns: ['description'], |         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 { favorite, unfavorite } = useFavoriteFeaturesApi(); | ||||||
|     const onFavorite = useCallback( |     const onFavorite = useCallback( | ||||||
|         async (feature: any) => { |         async (feature: any) => { | ||||||
|  |             // FIXME: projectId is missing
 | ||||||
|             try { |             try { | ||||||
|                 if (feature?.favorite) { |                 if (feature?.favorite) { | ||||||
|                     await unfavorite(feature.project, feature.name); |                     await unfavorite(feature.project, feature.name); | ||||||
| @ -122,8 +148,15 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|             { |             { | ||||||
|                 Header: ( |                 Header: ( | ||||||
|                     <FavoriteIconHeader |                     <FavoriteIconHeader | ||||||
|                         isActive={isFavoritesPinned} |                         isActive={tableState.favorites === 'true'} | ||||||
|                         onClick={onChangeIsFavoritePinned} |                         onClick={() => | ||||||
|  |                             setTableState({ | ||||||
|  |                                 favorites: | ||||||
|  |                                     tableState.favorites === 'true' | ||||||
|  |                                         ? 'false' | ||||||
|  |                                         : 'true', | ||||||
|  |                             }) | ||||||
|  |                         } | ||||||
|                     /> |                     /> | ||||||
|                 ), |                 ), | ||||||
|                 accessor: 'favorite', |                 accessor: 'favorite', | ||||||
| @ -194,38 +227,22 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                 Cell: FeatureStaleCell, |                 Cell: FeatureStaleCell, | ||||||
|                 sortType: 'boolean', |                 sortType: 'boolean', | ||||||
|                 maxWidth: 120, |                 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( |     const data = useMemo( | ||||||
|         () => |         () => | ||||||
|             searchedData?.length === 0 && loading |             features?.length === 0 && loading ? featuresPlaceholder : features, | ||||||
|                 ? featuresPlaceholder |         [features, loading], | ||||||
|                 : searchedData, |  | ||||||
|         [searchedData, loading], |  | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const { |     const { | ||||||
|         headerGroups, |         headerGroups, | ||||||
|         rows, |         rows, | ||||||
|         prepareRow, |         prepareRow, | ||||||
|         state: { sortBy }, |         state: { pageIndex, pageSize, sortBy }, | ||||||
|         setHiddenColumns, |         setHiddenColumns, | ||||||
|     } = useTable( |     } = useTable( | ||||||
|         { |         { | ||||||
| @ -237,11 +254,23 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|             autoResetSortBy: false, |             autoResetSortBy: false, | ||||||
|             disableSortRemove: true, |             disableSortRemove: true, | ||||||
|             disableMultiSort: true, |             disableMultiSort: true, | ||||||
|  |             manualSortBy: true, | ||||||
|  |             manualPagination: true, | ||||||
|         }, |         }, | ||||||
|         useSortBy, |         useSortBy, | ||||||
|         useFlexLayout, |         useFlexLayout, | ||||||
|  |         usePagination, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         setTableState({ | ||||||
|  |             page: `${pageIndex + 1}`, | ||||||
|  |             pageSize: `${pageSize}`, | ||||||
|  |             sortBy: sortBy[0]?.id || 'createdAt', | ||||||
|  |             sortOrder: sortBy[0]?.desc ? 'desc' : 'asc', | ||||||
|  |         }); | ||||||
|  |     }, [pageIndex, pageSize, sortBy]); | ||||||
|  | 
 | ||||||
|     useConditionallyHiddenColumns( |     useConditionallyHiddenColumns( | ||||||
|         [ |         [ | ||||||
|             { |             { | ||||||
| @ -260,32 +289,7 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|         setHiddenColumns, |         setHiddenColumns, | ||||||
|         columns, |         columns, | ||||||
|     ); |     ); | ||||||
| 
 |     const setSearchValue = (search = '') => setTableState({ search }); | ||||||
|     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)) { |     if (!(environments.length > 0)) { | ||||||
|         return null; |         return null; | ||||||
| @ -308,12 +312,10 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                                 show={ |                                 show={ | ||||||
|                                     <> |                                     <> | ||||||
|                                         <Search |                                         <Search | ||||||
|                                             placeholder='Search and Filter' |                                             placeholder='Search' | ||||||
|                                             expandable |                                             expandable | ||||||
|                                             initialValue={searchValue} |                                             initialValue={tableState.search} | ||||||
|                                             onChange={setSearchValue} |                                             onChange={setSearchValue} | ||||||
|                                             hasFilters |  | ||||||
|                                             getSearchContext={getSearchContext} |  | ||||||
|                                         /> |                                         /> | ||||||
|                                         <PageHeader.Divider /> |                                         <PageHeader.Divider /> | ||||||
|                                     </> |                                     </> | ||||||
| @ -361,38 +363,16 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                         condition={isSmallScreen} |                         condition={isSmallScreen} | ||||||
|                         show={ |                         show={ | ||||||
|                             <Search |                             <Search | ||||||
|                                 initialValue={searchValue} |                                 initialValue={tableState.search} | ||||||
|                                 onChange={setSearchValue} |                                 onChange={setSearchValue} | ||||||
|                                 hasFilters |  | ||||||
|                                 getSearchContext={getSearchContext} |  | ||||||
|                             /> |                             /> | ||||||
|                         } |                         } | ||||||
|                     /> |                     /> | ||||||
|                 </PageHeader> |                 </PageHeader> | ||||||
|             } |             } | ||||||
|         > |         > | ||||||
|             {featureSearchFrontend && ( |             <FeatureToggleFilters state={tableState} onChange={setTableState} /> | ||||||
|                 <Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}> |             <SearchHighlightProvider value={tableState.search || ''}> | ||||||
|                     <FilterItem |  | ||||||
|                         label='Project' |  | ||||||
|                         options={[ |  | ||||||
|                             { |  | ||||||
|                                 label: 'Project 1', |  | ||||||
|                                 value: '1', |  | ||||||
|                             }, |  | ||||||
|                             { |  | ||||||
|                                 label: 'Test', |  | ||||||
|                                 value: '2', |  | ||||||
|                             }, |  | ||||||
|                             { |  | ||||||
|                                 label: 'Default', |  | ||||||
|                                 value: '3', |  | ||||||
|                             }, |  | ||||||
|                         ]} |  | ||||||
|                     /> |  | ||||||
|                 </Box> |  | ||||||
|             )} |  | ||||||
|             <SearchHighlightProvider value={getSearchText(searchValue)}> |  | ||||||
|                 <VirtualizedTable |                 <VirtualizedTable | ||||||
|                     rows={rows} |                     rows={rows} | ||||||
|                     headerGroups={headerGroups} |                     headerGroups={headerGroups} | ||||||
| @ -403,11 +383,11 @@ export const FeatureToggleListTable: VFC = () => { | |||||||
|                 condition={rows.length === 0} |                 condition={rows.length === 0} | ||||||
|                 show={ |                 show={ | ||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={searchValue?.length > 0} |                         condition={(tableState.search || '')?.length > 0} | ||||||
|                         show={ |                         show={ | ||||||
|                             <TablePlaceholder> |                             <TablePlaceholder> | ||||||
|                                 No feature toggles found matching “ |                                 No feature toggles found matching “ | ||||||
|                                 {searchValue} |                                 {tableState.search} | ||||||
|                                 ” |                                 ” | ||||||
|                             </TablePlaceholder> |                             </TablePlaceholder> | ||||||
|                         } |                         } | ||||||
|  | |||||||
| @ -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<string> = { 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: ( | ||||||
|  |                     <FavoriteIconHeader | ||||||
|  |                         isActive={isFavoritesPinned} | ||||||
|  |                         onClick={onChangeIsFavoritePinned} | ||||||
|  |                     /> | ||||||
|  |                 ), | ||||||
|  |                 accessor: 'favorite', | ||||||
|  |                 Cell: ({ row: { original: feature } }: any) => ( | ||||||
|  |                     <FavoriteIconCell | ||||||
|  |                         value={feature?.favorite} | ||||||
|  |                         onClick={() => onFavorite(feature)} | ||||||
|  |                     /> | ||||||
|  |                 ), | ||||||
|  |                 maxWidth: 50, | ||||||
|  |                 disableSortBy: true, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 Header: 'Seen', | ||||||
|  |                 accessor: 'lastSeenAt', | ||||||
|  |                 Cell: ({ value, row: { original: feature } }: any) => { | ||||||
|  |                     return <FeatureEnvironmentSeenCell feature={feature} />; | ||||||
|  |                 }, | ||||||
|  |                 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 }) => ( | ||||||
|  |                     <LinkCell title={value} to={`/projects/${value}`} /> | ||||||
|  |                 ), | ||||||
|  |                 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 ( | ||||||
|  |         <PageContent | ||||||
|  |             isLoading={loading} | ||||||
|  |             header={ | ||||||
|  |                 <PageHeader | ||||||
|  |                     title={`Feature toggles (${ | ||||||
|  |                         rows.length < data.length | ||||||
|  |                             ? `${rows.length} of ${data.length}` | ||||||
|  |                             : data.length | ||||||
|  |                     })`}
 | ||||||
|  |                     actions={ | ||||||
|  |                         <> | ||||||
|  |                             <ConditionallyRender | ||||||
|  |                                 condition={!isSmallScreen} | ||||||
|  |                                 show={ | ||||||
|  |                                     <> | ||||||
|  |                                         <Search | ||||||
|  |                                             placeholder='Search and Filter' | ||||||
|  |                                             expandable | ||||||
|  |                                             initialValue={searchValue} | ||||||
|  |                                             onChange={setSearchValue} | ||||||
|  |                                             hasFilters | ||||||
|  |                                             getSearchContext={getSearchContext} | ||||||
|  |                                         /> | ||||||
|  |                                         <PageHeader.Divider /> | ||||||
|  |                                     </> | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  |                             <Link | ||||||
|  |                                 component={RouterLink} | ||||||
|  |                                 to='/archive' | ||||||
|  |                                 underline='always' | ||||||
|  |                                 sx={{ marginRight: 2, ...focusable(theme) }} | ||||||
|  |                             > | ||||||
|  |                                 View archive | ||||||
|  |                             </Link> | ||||||
|  |                             <ConditionallyRender | ||||||
|  |                                 condition={Boolean( | ||||||
|  |                                     uiConfig?.flags?.featuresExportImport, | ||||||
|  |                                 )} | ||||||
|  |                                 show={ | ||||||
|  |                                     <Tooltip | ||||||
|  |                                         title='Export current selection' | ||||||
|  |                                         arrow | ||||||
|  |                                     > | ||||||
|  |                                         <IconButton | ||||||
|  |                                             onClick={() => | ||||||
|  |                                                 setShowExportDialog(true) | ||||||
|  |                                             } | ||||||
|  |                                             sx={(theme) => ({ | ||||||
|  |                                                 marginRight: theme.spacing(2), | ||||||
|  |                                             })} | ||||||
|  |                                         > | ||||||
|  |                                             <FileDownload /> | ||||||
|  |                                         </IconButton> | ||||||
|  |                                     </Tooltip> | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  | 
 | ||||||
|  |                             <CreateFeatureButton | ||||||
|  |                                 loading={false} | ||||||
|  |                                 filter={{ query: '', project: 'default' }} | ||||||
|  |                             /> | ||||||
|  |                         </> | ||||||
|  |                     } | ||||||
|  |                 > | ||||||
|  |                     <ConditionallyRender | ||||||
|  |                         condition={isSmallScreen} | ||||||
|  |                         show={ | ||||||
|  |                             <Search | ||||||
|  |                                 initialValue={searchValue} | ||||||
|  |                                 onChange={setSearchValue} | ||||||
|  |                                 hasFilters | ||||||
|  |                                 getSearchContext={getSearchContext} | ||||||
|  |                             /> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 </PageHeader> | ||||||
|  |             } | ||||||
|  |         > | ||||||
|  |             <SearchHighlightProvider value={getSearchText(searchValue)}> | ||||||
|  |                 <VirtualizedTable | ||||||
|  |                     rows={rows} | ||||||
|  |                     headerGroups={headerGroups} | ||||||
|  |                     prepareRow={prepareRow} | ||||||
|  |                 /> | ||||||
|  |             </SearchHighlightProvider> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={rows.length === 0} | ||||||
|  |                 show={ | ||||||
|  |                     <ConditionallyRender | ||||||
|  |                         condition={searchValue?.length > 0} | ||||||
|  |                         show={ | ||||||
|  |                             <TablePlaceholder> | ||||||
|  |                                 No feature toggles found matching “ | ||||||
|  |                                 {searchValue} | ||||||
|  |                                 ” | ||||||
|  |                             </TablePlaceholder> | ||||||
|  |                         } | ||||||
|  |                         elseShow={ | ||||||
|  |                             <TablePlaceholder> | ||||||
|  |                                 No feature toggles available. Get started by | ||||||
|  |                                 adding a new feature toggle. | ||||||
|  |                             </TablePlaceholder> | ||||||
|  |                         } | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={Boolean(uiConfig?.flags?.featuresExportImport)} | ||||||
|  |                 show={ | ||||||
|  |                     <ExportDialog | ||||||
|  |                         showExportDialog={showExportDialog} | ||||||
|  |                         data={data} | ||||||
|  |                         onClose={() => setShowExportDialog(false)} | ||||||
|  |                         environments={enabledEnvironments} | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |         </PageContent> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -115,6 +115,7 @@ const StyledIconButton = styled(IconButton)<{ | |||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const Header: VFC = () => { | const Header: VFC = () => { | ||||||
|  |     const featureSearchFrontend = useUiFlag('featureSearchFrontend'); | ||||||
|     const { onSetThemeMode, themeMode } = useThemeMode(); |     const { onSetThemeMode, themeMode } = useThemeMode(); | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
|     const adminId = useId(); |     const adminId = useId(); | ||||||
| @ -191,7 +192,15 @@ const Header: VFC = () => { | |||||||
|                 <StyledNav> |                 <StyledNav> | ||||||
|                     <StyledLinks> |                     <StyledLinks> | ||||||
|                         <StyledLink to='/projects'>Projects</StyledLink> |                         <StyledLink to='/projects'>Projects</StyledLink> | ||||||
|                         <StyledLink to='/features'>Feature toggles</StyledLink> |                         <StyledLink | ||||||
|  |                             to={ | ||||||
|  |                                 featureSearchFrontend | ||||||
|  |                                     ? '/features-new' | ||||||
|  |                                     : '/features' | ||||||
|  |                             } | ||||||
|  |                         > | ||||||
|  |                             Feature toggles | ||||||
|  |                         </StyledLink> | ||||||
|                         <StyledLink to='/playground'>Playground</StyledLink> |                         <StyledLink to='/playground'>Playground</StyledLink> | ||||||
|                         <StyledAdvancedNavButton |                         <StyledAdvancedNavButton | ||||||
|                             onClick={(e) => setConfigRef(e.currentTarget)} |                             onClick={(e) => setConfigRef(e.currentTarget)} | ||||||
|  | |||||||
| @ -123,6 +123,16 @@ exports[`returns all baseRoutes 1`] = ` | |||||||
|     "title": "Feature toggles", |     "title": "Feature toggles", | ||||||
|     "type": "protected", |     "type": "protected", | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     "component": [Function], | ||||||
|  |     "flag": "featureSearchFrontend", | ||||||
|  |     "menu": { | ||||||
|  |       "mobile": true, | ||||||
|  |     }, | ||||||
|  |     "path": "/features-new", | ||||||
|  |     "title": "Feature toggles", | ||||||
|  |     "type": "protected", | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     "component": { |     "component": { | ||||||
|       "$$typeof": Symbol(react.lazy), |       "$$typeof": Symbol(react.lazy), | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable'; | import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable'; | ||||||
|  | import { FeatureToggleListTable as LegacyFeatureToggleListTable } from 'component/feature/FeatureToggleList/LegacyFeatureToggleListTable'; | ||||||
| import { StrategyView } from 'component/strategies/StrategyView/StrategyView'; | import { StrategyView } from 'component/strategies/StrategyView/StrategyView'; | ||||||
| import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList'; | import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList'; | ||||||
| import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList'; | import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList'; | ||||||
| @ -144,9 +145,17 @@ export const routes: IRoute[] = [ | |||||||
|     { |     { | ||||||
|         path: '/features', |         path: '/features', | ||||||
|         title: 'Feature toggles', |         title: 'Feature toggles', | ||||||
|  |         component: LegacyFeatureToggleListTable, | ||||||
|  |         type: 'protected', | ||||||
|  |         menu: { mobile: true }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: '/features-new', | ||||||
|  |         title: 'Feature toggles', | ||||||
|         component: FeatureToggleListTable, |         component: FeatureToggleListTable, | ||||||
|         type: 'protected', |         type: 'protected', | ||||||
|         menu: { mobile: true }, |         menu: { mobile: true }, | ||||||
|  |         flag: 'featureSearchFrontend', | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     // Playground
 |     // Playground
 | ||||||
|  | |||||||
| @ -128,7 +128,6 @@ const getFeatureSearchFetcher = ( | |||||||
|     const searchQueryParams = translateToQueryParams(searchValue); |     const searchQueryParams = translateToQueryParams(searchValue); | ||||||
|     const sortQueryParams = translateToSortQueryParams(sortingRules); |     const sortQueryParams = translateToSortQueryParams(sortingRules); | ||||||
|     const project = projectId ? `projectId=${projectId}&` : ''; |     const project = projectId ? `projectId=${projectId}&` : ''; | ||||||
| 
 |  | ||||||
|     const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`; |     const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`; | ||||||
|     const fetcher = () => { |     const fetcher = () => { | ||||||
|         const path = formatApiPath(KEY); |         const path = formatApiPath(KEY); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user