mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: connect search and filter with server api (#5297)
This commit is contained in:
		
							parent
							
								
									fa85e10eac
								
							
						
					
					
						commit
						24f9fa3058
					
				| @ -0,0 +1,666 @@ | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { | ||||
|     Checkbox, | ||||
|     IconButton, | ||||
|     styled, | ||||
|     Tooltip, | ||||
|     useMediaQuery, | ||||
|     useTheme, | ||||
| } 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 { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; | ||||
| import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; | ||||
| import { IProject } from 'interfaces/project'; | ||||
| import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; | ||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| import { createLocalStorage } from 'utils/createLocalStorage'; | ||||
| import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog'; | ||||
| 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 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; | ||||
| import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; | ||||
| import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { ListItemType } from './ProjectFeatureToggles.types'; | ||||
| import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell'; | ||||
| import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch'; | ||||
| 
 | ||||
| const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ | ||||
|     whiteSpace: 'nowrap', | ||||
| })); | ||||
| 
 | ||||
| interface IPaginatedProjectFeatureTogglesProps { | ||||
|     features: IProject['features']; | ||||
|     environments: IProject['environments']; | ||||
|     loading: boolean; | ||||
|     onChange: () => void; | ||||
|     total?: number; | ||||
|     searchValue: string; | ||||
|     setSearchValue: React.Dispatch<React.SetStateAction<string>>; | ||||
| } | ||||
| 
 | ||||
| const staticColumns = ['Select', 'Actions', 'name', 'favorite']; | ||||
| 
 | ||||
| const defaultSort: SortingRule<string> & { | ||||
|     columns?: string[]; | ||||
| } = { id: 'createdAt' }; | ||||
| 
 | ||||
| export const PaginatedProjectFeatureToggles = ({ | ||||
|     features, | ||||
|     loading, | ||||
|     environments: newEnvironments = [], | ||||
|     onChange, | ||||
|     total, | ||||
|     searchValue, | ||||
|     setSearchValue, | ||||
| }: IPaginatedProjectFeatureTogglesProps) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const theme = useTheme(); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||
|     const [strategiesDialogState, setStrategiesDialogState] = useState({ | ||||
|         open: false, | ||||
|         featureId: '', | ||||
|         environmentName: '', | ||||
|     }); | ||||
|     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 showEnvironmentLastSeen = Boolean( | ||||
|         uiConfig.flags.lastSeenByEnvironment, | ||||
|     ); | ||||
| 
 | ||||
|     const onFavorite = useCallback( | ||||
|         async (feature: IFeatureToggleListItem) => { | ||||
|             if (feature?.favorite) { | ||||
|                 await unfavorite(projectId, feature.name); | ||||
|             } else { | ||||
|                 await favorite(projectId, feature.name); | ||||
|             } | ||||
|             onChange(); | ||||
|         }, | ||||
|         [projectId, onChange], | ||||
|     ); | ||||
| 
 | ||||
|     const showTagsColumn = useMemo( | ||||
|         () => features.some((feature) => feature?.tags?.length), | ||||
|         [features], | ||||
|     ); | ||||
| 
 | ||||
|     const columns = useMemo( | ||||
|         () => [ | ||||
|             { | ||||
|                 id: 'Select', | ||||
|                 Header: ({ getToggleAllRowsSelectedProps }: any) => ( | ||||
|                     <Checkbox {...getToggleAllRowsSelectedProps()} /> | ||||
|                 ), | ||||
|                 Cell: ({ row }: any) => ( | ||||
|                     <MemoizedRowSelectCell | ||||
|                         {...row?.getToggleRowSelectedProps?.()} | ||||
|                     /> | ||||
|                 ), | ||||
|                 maxWidth: 50, | ||||
|                 disableSortBy: true, | ||||
|                 hideInMenu: true, | ||||
|             }, | ||||
|             { | ||||
|                 id: 'favorite', | ||||
|                 Header: ( | ||||
|                     <FavoriteIconHeader | ||||
|                         isActive={isFavoritesPinned} | ||||
|                         onClick={onChangeIsFavoritePinned} | ||||
|                     /> | ||||
|                 ), | ||||
|                 accessor: 'favorite', | ||||
|                 Cell: ({ row: { original: feature } }: any) => ( | ||||
|                     <FavoriteIconCell | ||||
|                         value={feature?.favorite} | ||||
|                         onClick={() => onFavorite(feature)} | ||||
|                     /> | ||||
|                 ), | ||||
|                 maxWidth: 50, | ||||
|                 disableSortBy: true, | ||||
|                 hideInMenu: true, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Seen', | ||||
|                 accessor: 'lastSeenAt', | ||||
|                 Cell: ({ value, row: { original: feature } }: any) => { | ||||
|                     return showEnvironmentLastSeen ? ( | ||||
|                         <MemoizedFeatureEnvironmentSeenCell feature={feature} /> | ||||
|                     ) : ( | ||||
|                         <FeatureSeenCell value={value} /> | ||||
|                     ); | ||||
|                 }, | ||||
|                 align: 'center', | ||||
|                 maxWidth: 80, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Type', | ||||
|                 accessor: 'type', | ||||
|                 Cell: FeatureTypeCell, | ||||
|                 align: 'center', | ||||
|                 filterName: 'type', | ||||
|                 maxWidth: 80, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Name', | ||||
|                 accessor: 'name', | ||||
|                 Cell: ({ value }: { value: string }) => ( | ||||
|                     <Tooltip title={value} arrow describeChild> | ||||
|                         <span> | ||||
|                             <LinkCell | ||||
|                                 title={value} | ||||
|                                 to={`/projects/${projectId}/features/${value}`} | ||||
|                             /> | ||||
|                         </span> | ||||
|                     </Tooltip> | ||||
|                 ), | ||||
|                 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, | ||||
|                 sortType: 'date', | ||||
|                 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 } }) => ( | ||||
|                     <ActionsCell | ||||
|                         projectId={projectId} | ||||
|                         onOpenArchiveDialog={setFeatureArchiveState} | ||||
|                         onOpenStaleDialog={setFeatureStaleDialogState} | ||||
|                         {...props} | ||||
|                     /> | ||||
|                 ), | ||||
|                 disableSortBy: true, | ||||
|                 hideInMenu: true, | ||||
|             }, | ||||
|         ], | ||||
|         [projectId, environments, loading], | ||||
|     ); | ||||
| 
 | ||||
|     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 { 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 featuresData; | ||||
|     }, [loading, featuresData]); | ||||
| 
 | ||||
|     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) => `environments.${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') || '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<string, string> = {}; | ||||
|         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 ( | ||||
|         <> | ||||
|             <PageContent | ||||
|                 isLoading={loading} | ||||
|                 className={styles.container} | ||||
|                 header={ | ||||
|                     <PageHeader | ||||
|                         titleElement={ | ||||
|                             showTitle | ||||
|                                 ? `Feature toggles (${total || rows.length})` | ||||
|                                 : null | ||||
|                         } | ||||
|                         actions={ | ||||
|                             <> | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={!isSmallScreen} | ||||
|                                     show={ | ||||
|                                         <Search | ||||
|                                             placeholder='Search and Filter' | ||||
|                                             expandable | ||||
|                                             initialValue={searchValue} | ||||
|                                             onChange={setSearchValue} | ||||
|                                             onFocus={() => setShowTitle(false)} | ||||
|                                             onBlur={() => setShowTitle(true)} | ||||
|                                             hasFilters | ||||
|                                             getSearchContext={getSearchContext} | ||||
|                                             id='projectFeatureToggles' | ||||
|                                         /> | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <ColumnsMenu | ||||
|                                     allColumns={allColumns} | ||||
|                                     staticColumns={staticColumns} | ||||
|                                     dividerAfter={['createdAt']} | ||||
|                                     dividerBefore={['Actions']} | ||||
|                                     isCustomized={Boolean(storedParams.columns)} | ||||
|                                     setHiddenColumns={setHiddenColumns} | ||||
|                                 /> | ||||
|                                 <PageHeader.Divider sx={{ marginLeft: 0 }} /> | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={Boolean( | ||||
|                                         uiConfig?.flags?.featuresExportImport, | ||||
|                                     )} | ||||
|                                     show={ | ||||
|                                         <Tooltip | ||||
|                                             title='Export toggles visible in the table below' | ||||
|                                             arrow | ||||
|                                         > | ||||
|                                             <IconButton | ||||
|                                                 onClick={() => | ||||
|                                                     setShowExportDialog(true) | ||||
|                                                 } | ||||
|                                                 sx={(theme) => ({ | ||||
|                                                     marginRight: | ||||
|                                                         theme.spacing(2), | ||||
|                                                 })} | ||||
|                                             > | ||||
|                                                 <FileDownload /> | ||||
|                                             </IconButton> | ||||
|                                         </Tooltip> | ||||
|                                     } | ||||
|                                 /> | ||||
|                                 <StyledResponsiveButton | ||||
|                                     onClick={() => | ||||
|                                         navigate(getCreateTogglePath(projectId)) | ||||
|                                     } | ||||
|                                     maxWidth='960px' | ||||
|                                     Icon={Add} | ||||
|                                     projectId={projectId} | ||||
|                                     permission={CREATE_FEATURE} | ||||
|                                     data-testid='NAVIGATE_TO_CREATE_FEATURE' | ||||
|                                 > | ||||
|                                     New feature toggle | ||||
|                                 </StyledResponsiveButton> | ||||
|                             </> | ||||
|                         } | ||||
|                     > | ||||
|                         <ConditionallyRender | ||||
|                             condition={isSmallScreen} | ||||
|                             show={ | ||||
|                                 <Search | ||||
|                                     initialValue={searchValue} | ||||
|                                     onChange={setSearchValue} | ||||
|                                     hasFilters | ||||
|                                     getSearchContext={getSearchContext} | ||||
|                                     id='projectFeatureToggles' | ||||
|                                 /> | ||||
|                             } | ||||
|                         /> | ||||
|                     </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> | ||||
|                             } | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|                 <EnvironmentStrategyDialog | ||||
|                     onClose={() => | ||||
|                         setStrategiesDialogState((prev) => ({ | ||||
|                             ...prev, | ||||
|                             open: false, | ||||
|                         })) | ||||
|                     } | ||||
|                     projectId={projectId} | ||||
|                     {...strategiesDialogState} | ||||
|                 /> | ||||
|                 <FeatureStaleDialog | ||||
|                     isStale={featureStaleDialogState.stale === true} | ||||
|                     isOpen={Boolean(featureStaleDialogState.featureId)} | ||||
|                     onClose={() => { | ||||
|                         setFeatureStaleDialogState({}); | ||||
|                         onChange(); | ||||
|                     }} | ||||
|                     featureId={featureStaleDialogState.featureId || ''} | ||||
|                     projectId={projectId} | ||||
|                 /> | ||||
|                 <FeatureArchiveDialog | ||||
|                     isOpen={Boolean(featureArchiveState)} | ||||
|                     onConfirm={onChange} | ||||
|                     onClose={() => { | ||||
|                         setFeatureArchiveState(undefined); | ||||
|                     }} | ||||
|                     featureIds={[featureArchiveState || '']} | ||||
|                     projectId={projectId} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={ | ||||
|                         Boolean(uiConfig?.flags?.featuresExportImport) && | ||||
|                         !loading | ||||
|                     } | ||||
|                     show={ | ||||
|                         <ExportDialog | ||||
|                             showExportDialog={showExportDialog} | ||||
|                             data={data} | ||||
|                             onClose={() => setShowExportDialog(false)} | ||||
|                             environments={environments} | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|                 {featureToggleModals} | ||||
|             </PageContent> | ||||
|             <BatchSelectionActionsBar | ||||
|                 count={Object.keys(selectedRowIds).length} | ||||
|             > | ||||
|                 <ProjectFeaturesBatchActions | ||||
|                     selectedIds={Object.keys(selectedRowIds)} | ||||
|                     data={features} | ||||
|                     projectId={projectId} | ||||
|                     onResetSelection={() => toggleAllRowsSelected(false)} | ||||
|                 /> | ||||
|             </BatchSelectionActionsBar> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -83,6 +83,9 @@ const defaultSort: SortingRule<string> & { | ||||
|     columns?: string[]; | ||||
| } = { id: 'createdAt' }; | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated remove when flag `featureSearchFrontend` is removed | ||||
|  */ | ||||
| export const ProjectFeatureToggles = ({ | ||||
|     features, | ||||
|     loading, | ||||
|  | ||||
| @ -13,6 +13,8 @@ import { ProjectStats } from './ProjectStats/ProjectStats'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; | ||||
| import { PaginatedProjectFeatureToggles } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles'; | ||||
| import { useSearchParams } from 'react-router-dom'; | ||||
| 
 | ||||
| const refreshInterval = 15 * 1000; | ||||
| 
 | ||||
| @ -39,16 +41,22 @@ const PAGE_LIMIT = 25; | ||||
| 
 | ||||
| const PaginatedProjectOverview = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const [searchParams, setSearchParams] = useSearchParams(); | ||||
|     const { project, loading: projectLoading } = useProject(projectId, { | ||||
|         refreshInterval, | ||||
|     }); | ||||
|     const [currentOffset, setCurrentOffset] = useState(0); | ||||
| 
 | ||||
|     const [searchValue, setSearchValue] = useState( | ||||
|         searchParams.get('search') || '', | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
|         features: searchFeatures, | ||||
|         total, | ||||
|         refetch, | ||||
|         loading, | ||||
|     } = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, { | ||||
|     } = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, searchValue, { | ||||
|         refreshInterval, | ||||
|     }); | ||||
| 
 | ||||
| @ -79,7 +87,7 @@ const PaginatedProjectOverview = () => { | ||||
|             <StyledContentContainer> | ||||
|                 <ProjectStats stats={project.stats} /> | ||||
|                 <StyledProjectToggles> | ||||
|                     <ProjectFeatureToggles | ||||
|                     <PaginatedProjectFeatureToggles | ||||
|                         key={ | ||||
|                             loading && searchFeatures.length === 0 | ||||
|                                 ? 'loading' | ||||
| @ -90,6 +98,8 @@ const PaginatedProjectOverview = () => { | ||||
|                         loading={loading && searchFeatures.length === 0} | ||||
|                         onChange={refetch} | ||||
|                         total={total} | ||||
|                         searchValue={searchValue} | ||||
|                         setSearchValue={setSearchValue} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={hasPreviousPage} | ||||
| @ -105,6 +115,9 @@ const PaginatedProjectOverview = () => { | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated remove when flag `featureSearchFrontend` is removed | ||||
|  */ | ||||
| const ProjectOverview = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|  | ||||
| @ -36,10 +36,11 @@ describe('translateToQueryParams', () => { | ||||
|             'development:enabled,disabled', | ||||
|             'status[]=development:enabled&status[]=development:disabled', | ||||
|         ], | ||||
|         ['tag:simple:web', 'tag[]=simple:web'], | ||||
|         ['tag:enabled:enabled', 'tag[]=enabled:enabled'], | ||||
|         ['tags:simple:web', 'tag[]=simple:web'], | ||||
|         ['tags:enabled:enabled', 'tag[]=enabled:enabled'], | ||||
|         ['tags:simp', 'tag[]=simp'], | ||||
|         [ | ||||
|             'tag:simple:web,complex:native', | ||||
|             'tags:simple:web,complex:native', | ||||
|             'tag[]=simple:web&tag[]=complex:native', | ||||
|         ], | ||||
|     ])('when input is "%s"', (input, expected) => { | ||||
|  | ||||
| @ -45,7 +45,7 @@ const handleFilter = ( | ||||
| 
 | ||||
|     if (isStatusFilter(key, values)) { | ||||
|         return addStatusFilters(key, values, filterParams); | ||||
|     } else if (key === 'tag') { | ||||
|     } else if (key === 'tags') { | ||||
|         return addTagFilters(values, filterParams); | ||||
|     } else { | ||||
|         return addRegularFilters(key, values, filterParams); | ||||
|  | ||||
| @ -3,6 +3,7 @@ import { useCallback } from 'react'; | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | ||||
| import { translateToQueryParams } from './searchToQueryParams'; | ||||
| 
 | ||||
| type IFeatureSearchResponse = { | ||||
|     features: IFeatureToggleListItem[]; | ||||
| @ -29,9 +30,15 @@ export const useFeatureSearch = ( | ||||
|     offset: number, | ||||
|     limit: number, | ||||
|     projectId = '', | ||||
|     searchValue = '', | ||||
|     options: SWRConfiguration = {}, | ||||
| ): IUseFeatureSearchOutput => { | ||||
|     const { KEY, fetcher } = getFeatureSearchFetcher(projectId, offset, limit); | ||||
|     const { KEY, fetcher } = getFeatureSearchFetcher( | ||||
|         projectId, | ||||
|         offset, | ||||
|         limit, | ||||
|         searchValue, | ||||
|     ); | ||||
|     const { data, error, mutate } = useSWR<IFeatureSearchResponse>( | ||||
|         KEY, | ||||
|         fetcher, | ||||
| @ -45,7 +52,7 @@ export const useFeatureSearch = ( | ||||
|     const returnData = data || fallbackData; | ||||
|     return { | ||||
|         ...returnData, | ||||
|         loading: !error && !data, | ||||
|         loading: false, | ||||
|         error, | ||||
|         refetch, | ||||
|     }; | ||||
| @ -55,9 +62,10 @@ const getFeatureSearchFetcher = ( | ||||
|     projectId: string, | ||||
|     offset: number, | ||||
|     limit: number, | ||||
|     searchValue: string, | ||||
| ) => { | ||||
|     const KEY = `api/admin/search/features?projectId=${projectId}&offset=${offset}&limit=${limit}`; | ||||
| 
 | ||||
|     const searchQueryParams = translateToQueryParams(searchValue); | ||||
|     const KEY = `api/admin/search/features?projectId=${projectId}&offset=${offset}&limit=${limit}&${searchQueryParams}`; | ||||
|     const fetcher = () => { | ||||
|         const path = formatApiPath(KEY); | ||||
|         return fetch(path, { | ||||
|  | ||||
| @ -21,7 +21,6 @@ export class FeatureSearchService { | ||||
|     } | ||||
| 
 | ||||
|     async search(params: IFeatureSearchParams) { | ||||
|         // fetch one more item than needed to get a cursor of the next item
 | ||||
|         const { features, total } = | ||||
|             await this.featureStrategiesStore.searchFeatures({ | ||||
|                 ...params, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user