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[]; |     columns?: string[]; | ||||||
| } = { id: 'createdAt' }; | } = { id: 'createdAt' }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated remove when flag `featureSearchFrontend` is removed | ||||||
|  |  */ | ||||||
| export const ProjectFeatureToggles = ({ | export const ProjectFeatureToggles = ({ | ||||||
|     features, |     features, | ||||||
|     loading, |     loading, | ||||||
|  | |||||||
| @ -13,6 +13,8 @@ import { ProjectStats } from './ProjectStats/ProjectStats'; | |||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { useUiFlag } from 'hooks/useUiFlag'; | import { useUiFlag } from 'hooks/useUiFlag'; | ||||||
| import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; | import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; | ||||||
|  | import { PaginatedProjectFeatureToggles } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles'; | ||||||
|  | import { useSearchParams } from 'react-router-dom'; | ||||||
| 
 | 
 | ||||||
| const refreshInterval = 15 * 1000; | const refreshInterval = 15 * 1000; | ||||||
| 
 | 
 | ||||||
| @ -39,16 +41,22 @@ const PAGE_LIMIT = 25; | |||||||
| 
 | 
 | ||||||
| const PaginatedProjectOverview = () => { | const PaginatedProjectOverview = () => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  |     const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|     const { project, loading: projectLoading } = useProject(projectId, { |     const { project, loading: projectLoading } = useProject(projectId, { | ||||||
|         refreshInterval, |         refreshInterval, | ||||||
|     }); |     }); | ||||||
|     const [currentOffset, setCurrentOffset] = useState(0); |     const [currentOffset, setCurrentOffset] = useState(0); | ||||||
|  | 
 | ||||||
|  |     const [searchValue, setSearchValue] = useState( | ||||||
|  |         searchParams.get('search') || '', | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     const { |     const { | ||||||
|         features: searchFeatures, |         features: searchFeatures, | ||||||
|         total, |         total, | ||||||
|         refetch, |         refetch, | ||||||
|         loading, |         loading, | ||||||
|     } = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, { |     } = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, searchValue, { | ||||||
|         refreshInterval, |         refreshInterval, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -79,7 +87,7 @@ const PaginatedProjectOverview = () => { | |||||||
|             <StyledContentContainer> |             <StyledContentContainer> | ||||||
|                 <ProjectStats stats={project.stats} /> |                 <ProjectStats stats={project.stats} /> | ||||||
|                 <StyledProjectToggles> |                 <StyledProjectToggles> | ||||||
|                     <ProjectFeatureToggles |                     <PaginatedProjectFeatureToggles | ||||||
|                         key={ |                         key={ | ||||||
|                             loading && searchFeatures.length === 0 |                             loading && searchFeatures.length === 0 | ||||||
|                                 ? 'loading' |                                 ? 'loading' | ||||||
| @ -90,6 +98,8 @@ const PaginatedProjectOverview = () => { | |||||||
|                         loading={loading && searchFeatures.length === 0} |                         loading={loading && searchFeatures.length === 0} | ||||||
|                         onChange={refetch} |                         onChange={refetch} | ||||||
|                         total={total} |                         total={total} | ||||||
|  |                         searchValue={searchValue} | ||||||
|  |                         setSearchValue={setSearchValue} | ||||||
|                     /> |                     /> | ||||||
|                     <ConditionallyRender |                     <ConditionallyRender | ||||||
|                         condition={hasPreviousPage} |                         condition={hasPreviousPage} | ||||||
| @ -105,6 +115,9 @@ const PaginatedProjectOverview = () => { | |||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated remove when flag `featureSearchFrontend` is removed | ||||||
|  |  */ | ||||||
| const ProjectOverview = () => { | const ProjectOverview = () => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     const projectName = useProjectNameOrId(projectId); |     const projectName = useProjectNameOrId(projectId); | ||||||
|  | |||||||
| @ -36,10 +36,11 @@ describe('translateToQueryParams', () => { | |||||||
|             'development:enabled,disabled', |             'development:enabled,disabled', | ||||||
|             'status[]=development:enabled&status[]=development:disabled', |             'status[]=development:enabled&status[]=development:disabled', | ||||||
|         ], |         ], | ||||||
|         ['tag:simple:web', 'tag[]=simple:web'], |         ['tags:simple:web', 'tag[]=simple:web'], | ||||||
|         ['tag:enabled:enabled', 'tag[]=enabled:enabled'], |         ['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', |             'tag[]=simple:web&tag[]=complex:native', | ||||||
|         ], |         ], | ||||||
|     ])('when input is "%s"', (input, expected) => { |     ])('when input is "%s"', (input, expected) => { | ||||||
|  | |||||||
| @ -45,7 +45,7 @@ const handleFilter = ( | |||||||
| 
 | 
 | ||||||
|     if (isStatusFilter(key, values)) { |     if (isStatusFilter(key, values)) { | ||||||
|         return addStatusFilters(key, values, filterParams); |         return addStatusFilters(key, values, filterParams); | ||||||
|     } else if (key === 'tag') { |     } else if (key === 'tags') { | ||||||
|         return addTagFilters(values, filterParams); |         return addTagFilters(values, filterParams); | ||||||
|     } else { |     } else { | ||||||
|         return addRegularFilters(key, values, filterParams); |         return addRegularFilters(key, values, filterParams); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import { useCallback } from 'react'; | |||||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||||
| import { formatApiPath } from 'utils/formatPath'; | import { formatApiPath } from 'utils/formatPath'; | ||||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | import handleErrorResponses from '../httpErrorResponseHandler'; | ||||||
|  | import { translateToQueryParams } from './searchToQueryParams'; | ||||||
| 
 | 
 | ||||||
| type IFeatureSearchResponse = { | type IFeatureSearchResponse = { | ||||||
|     features: IFeatureToggleListItem[]; |     features: IFeatureToggleListItem[]; | ||||||
| @ -29,9 +30,15 @@ export const useFeatureSearch = ( | |||||||
|     offset: number, |     offset: number, | ||||||
|     limit: number, |     limit: number, | ||||||
|     projectId = '', |     projectId = '', | ||||||
|  |     searchValue = '', | ||||||
|     options: SWRConfiguration = {}, |     options: SWRConfiguration = {}, | ||||||
| ): IUseFeatureSearchOutput => { | ): IUseFeatureSearchOutput => { | ||||||
|     const { KEY, fetcher } = getFeatureSearchFetcher(projectId, offset, limit); |     const { KEY, fetcher } = getFeatureSearchFetcher( | ||||||
|  |         projectId, | ||||||
|  |         offset, | ||||||
|  |         limit, | ||||||
|  |         searchValue, | ||||||
|  |     ); | ||||||
|     const { data, error, mutate } = useSWR<IFeatureSearchResponse>( |     const { data, error, mutate } = useSWR<IFeatureSearchResponse>( | ||||||
|         KEY, |         KEY, | ||||||
|         fetcher, |         fetcher, | ||||||
| @ -45,7 +52,7 @@ export const useFeatureSearch = ( | |||||||
|     const returnData = data || fallbackData; |     const returnData = data || fallbackData; | ||||||
|     return { |     return { | ||||||
|         ...returnData, |         ...returnData, | ||||||
|         loading: !error && !data, |         loading: false, | ||||||
|         error, |         error, | ||||||
|         refetch, |         refetch, | ||||||
|     }; |     }; | ||||||
| @ -55,9 +62,10 @@ const getFeatureSearchFetcher = ( | |||||||
|     projectId: string, |     projectId: string, | ||||||
|     offset: number, |     offset: number, | ||||||
|     limit: 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 fetcher = () => { | ||||||
|         const path = formatApiPath(KEY); |         const path = formatApiPath(KEY); | ||||||
|         return fetch(path, { |         return fetch(path, { | ||||||
|  | |||||||
| @ -21,7 +21,6 @@ export class FeatureSearchService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async search(params: IFeatureSearchParams) { |     async search(params: IFeatureSearchParams) { | ||||||
|         // fetch one more item than needed to get a cursor of the next item
 |  | ||||||
|         const { features, total } = |         const { features, total } = | ||||||
|             await this.featureStrategiesStore.searchFeatures({ |             await this.featureStrategiesStore.searchFeatures({ | ||||||
|                 ...params, |                 ...params, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user