mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: filter by created by (#7306)
This commit is contained in:
		
							parent
							
								
									bb3498adb6
								
							
						
					
					
						commit
						a91b77a7ce
					
				| @ -55,6 +55,8 @@ export const ProjectFeatureToggles = ({ | |||||||
|     environments, |     environments, | ||||||
| }: IPaginatedProjectFeatureTogglesProps) => { | }: IPaginatedProjectFeatureTogglesProps) => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  |     const featureLifecycleEnabled = useUiFlag('featureLifecycle'); | ||||||
|  |     const flagCreatorEnabled = useUiFlag('flagCreator'); | ||||||
| 
 | 
 | ||||||
|     const { |     const { | ||||||
|         features, |         features, | ||||||
| @ -75,6 +77,7 @@ export const ProjectFeatureToggles = ({ | |||||||
|         tag: tableState.tag, |         tag: tableState.tag, | ||||||
|         createdAt: tableState.createdAt, |         createdAt: tableState.createdAt, | ||||||
|         type: tableState.type, |         type: tableState.type, | ||||||
|  |         ...(flagCreatorEnabled ? { createdBy: tableState.createdBy } : {}), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const { favorite, unfavorite } = useFavoriteFeaturesApi(); |     const { favorite, unfavorite } = useFavoriteFeaturesApi(); | ||||||
| @ -101,9 +104,6 @@ export const ProjectFeatureToggles = ({ | |||||||
| 
 | 
 | ||||||
|     const isPlaceholder = Boolean(initialLoad || (loading && total)); |     const isPlaceholder = Boolean(initialLoad || (loading && total)); | ||||||
| 
 | 
 | ||||||
|     const featureLifecycleEnabled = useUiFlag('featureLifecycle'); |  | ||||||
|     const flagCreatorEnabled = useUiFlag('flagCreator'); |  | ||||||
| 
 |  | ||||||
|     const columns = useMemo( |     const columns = useMemo( | ||||||
|         () => [ |         () => [ | ||||||
|             columnHelper.display({ |             columnHelper.display({ | ||||||
| @ -490,6 +490,7 @@ export const ProjectFeatureToggles = ({ | |||||||
|                     aria-live='polite' |                     aria-live='polite' | ||||||
|                 > |                 > | ||||||
|                     <ProjectOverviewFilters |                     <ProjectOverviewFilters | ||||||
|  |                         project={projectId} | ||||||
|                         onChange={setTableState} |                         onChange={setTableState} | ||||||
|                         state={filterState} |                         state={filterState} | ||||||
|                     /> |                     /> | ||||||
|  | |||||||
| @ -5,18 +5,24 @@ import { | |||||||
|     Filters, |     Filters, | ||||||
|     type IFilterItem, |     type IFilterItem, | ||||||
| } from 'component/filter/Filters/Filters'; | } from 'component/filter/Filters/Filters'; | ||||||
|  | import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators'; | ||||||
|  | import { useUiFlag } from 'hooks/useUiFlag'; | ||||||
| 
 | 
 | ||||||
| interface IProjectOverviewFilters { | interface IProjectOverviewFilters { | ||||||
|     state: FilterItemParamHolder; |     state: FilterItemParamHolder; | ||||||
|     onChange: (value: FilterItemParamHolder) => void; |     onChange: (value: FilterItemParamHolder) => void; | ||||||
|  |     project: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({ | export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({ | ||||||
|     state, |     state, | ||||||
|     onChange, |     onChange, | ||||||
|  |     project, | ||||||
| }) => { | }) => { | ||||||
|     const { tags } = useAllTags(); |     const { tags } = useAllTags(); | ||||||
|  |     const { flagCreators } = useProjectFlagCreators(project); | ||||||
|     const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]); |     const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]); | ||||||
|  |     const flagCreatorEnabled = useUiFlag('flagCreator'); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const tagsOptions = (tags || []).map((tag) => ({ |         const tagsOptions = (tags || []).map((tag) => ({ | ||||||
| @ -24,6 +30,11 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({ | |||||||
|             value: `${tag.type}:${tag.value}`, |             value: `${tag.type}:${tag.value}`, | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  |         const flagCreatorsOptions = flagCreators.map((creator) => ({ | ||||||
|  |             label: creator.name, | ||||||
|  |             value: String(creator.id), | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|         const availableFilters: IFilterItem[] = [ |         const availableFilters: IFilterItem[] = [ | ||||||
|             { |             { | ||||||
|                 label: 'Tags', |                 label: 'Tags', | ||||||
| @ -60,9 +71,19 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({ | |||||||
|                 pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'], |                 pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'], | ||||||
|             }, |             }, | ||||||
|         ]; |         ]; | ||||||
|  |         if (flagCreatorEnabled) { | ||||||
|  |             availableFilters.push({ | ||||||
|  |                 label: 'Created by', | ||||||
|  |                 icon: 'person', | ||||||
|  |                 options: flagCreatorsOptions, | ||||||
|  |                 filterKey: 'createdBy', | ||||||
|  |                 singularOperators: ['IS', 'IS_NOT'], | ||||||
|  |                 pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'], | ||||||
|  |             }); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         setAvailableFilters(availableFilters); |         setAvailableFilters(availableFilters); | ||||||
|     }, [JSON.stringify(tags)]); |     }, [JSON.stringify(tags), JSON.stringify(flagCreators)]); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <Filters |         <Filters | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ import { | |||||||
| } from 'utils/serializeQueryParams'; | } from 'utils/serializeQueryParams'; | ||||||
| import { usePersistentTableState } from 'hooks/usePersistentTableState'; | import { usePersistentTableState } from 'hooks/usePersistentTableState'; | ||||||
| import mapValues from 'lodash.mapvalues'; | import mapValues from 'lodash.mapvalues'; | ||||||
|  | import { useUiFlag } from 'hooks/useUiFlag'; | ||||||
| 
 | 
 | ||||||
| type Attribute = | type Attribute = | ||||||
|     | { key: 'tag'; operator: 'INCLUDE' } |     | { key: 'tag'; operator: 'INCLUDE' } | ||||||
| @ -25,6 +26,7 @@ export const useProjectFeatureSearch = ( | |||||||
|     storageKey = 'project-overview-v2', |     storageKey = 'project-overview-v2', | ||||||
|     refreshInterval = 15 * 1000, |     refreshInterval = 15 * 1000, | ||||||
| ) => { | ) => { | ||||||
|  |     const flagCreatorEnabled = useUiFlag('flagCreator'); | ||||||
|     const stateConfig = { |     const stateConfig = { | ||||||
|         offset: withDefault(NumberParam, 0), |         offset: withDefault(NumberParam, 0), | ||||||
|         limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), |         limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), | ||||||
| @ -36,6 +38,7 @@ export const useProjectFeatureSearch = ( | |||||||
|         tag: FilterItemParam, |         tag: FilterItemParam, | ||||||
|         createdAt: FilterItemParam, |         createdAt: FilterItemParam, | ||||||
|         type: FilterItemParam, |         type: FilterItemParam, | ||||||
|  |         ...(flagCreatorEnabled ? { createdBy: FilterItemParam } : {}), | ||||||
|     }; |     }; | ||||||
|     const [tableState, setTableState] = usePersistentTableState( |     const [tableState, setTableState] = usePersistentTableState( | ||||||
|         `${storageKey}-${projectId}`, |         `${storageKey}-${projectId}`, | ||||||
|  | |||||||
| @ -0,0 +1,13 @@ | |||||||
|  | import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter'; | ||||||
|  | import { formatApiPath } from 'utils/formatPath'; | ||||||
|  | import type { ProjectFlagCreatorsSchema } from '../../../../openapi'; | ||||||
|  | 
 | ||||||
|  | export const useProjectFlagCreators = (project: string) => { | ||||||
|  |     const PATH = `api/admin/projects/${project}/flag-creators`; | ||||||
|  |     const { data, refetch, loading, error } = | ||||||
|  |         useApiGetter<ProjectFlagCreatorsSchema>(formatApiPath(PATH), () => | ||||||
|  |             fetcher(formatApiPath(PATH), 'Flag creators'), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |     return { flagCreators: data || [], refetch, error }; | ||||||
|  | }; | ||||||
| @ -82,6 +82,7 @@ export default class FeatureSearchController extends Controller { | |||||||
|             tag, |             tag, | ||||||
|             segment, |             segment, | ||||||
|             createdAt, |             createdAt, | ||||||
|  |             createdBy, | ||||||
|             state, |             state, | ||||||
|             status, |             status, | ||||||
|             favoritesFirst, |             favoritesFirst, | ||||||
| @ -116,6 +117,7 @@ export default class FeatureSearchController extends Controller { | |||||||
|             segment, |             segment, | ||||||
|             state, |             state, | ||||||
|             createdAt, |             createdAt, | ||||||
|  |             createdBy, | ||||||
|             status: normalizedStatus, |             status: normalizedStatus, | ||||||
|             offset: normalizedOffset, |             offset: normalizedOffset, | ||||||
|             limit: normalizedLimit, |             limit: normalizedLimit, | ||||||
|  | |||||||
| @ -75,6 +75,14 @@ export class FeatureSearchService { | |||||||
|             if (parsed) queryParams.push(parsed); |             if (parsed) queryParams.push(parsed); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (params.createdBy) { | ||||||
|  |             const parsed = this.parseOperatorValue( | ||||||
|  |                 'users.id', | ||||||
|  |                 params.createdBy, | ||||||
|  |             ); | ||||||
|  |             if (parsed) queryParams.push(parsed); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (params.type) { |         if (params.type) { | ||||||
|             const parsed = this.parseOperatorValue( |             const parsed = this.parseOperatorValue( | ||||||
|                 'features.type', |                 'features.type', | ||||||
|  | |||||||
| @ -108,6 +108,15 @@ const filterFeaturesByType = async (typeParams: string, expectedCode = 200) => { | |||||||
|         .expect(expectedCode); |         .expect(expectedCode); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const filterFeaturesByCreatedBy = async ( | ||||||
|  |     createdByParams: string, | ||||||
|  |     expectedCode = 200, | ||||||
|  | ) => { | ||||||
|  |     return app.request | ||||||
|  |         .get(`/api/admin/search/features?createdBy=${createdByParams}`) | ||||||
|  |         .expect(expectedCode); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const filterFeaturesByTag = async (tag: string, expectedCode = 200) => { | const filterFeaturesByTag = async (tag: string, expectedCode = 200) => { | ||||||
|     return app.request |     return app.request | ||||||
|         .get(`/api/admin/search/features?tag=${tag}`) |         .get(`/api/admin/search/features?tag=${tag}`) | ||||||
| @ -246,6 +255,29 @@ test('should filter features by type', async () => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('should filter features by created by', async () => { | ||||||
|  |     await app.createFeature({ | ||||||
|  |         name: 'my_feature_a', | ||||||
|  |         type: 'release', | ||||||
|  |     }); | ||||||
|  |     await app.createFeature({ | ||||||
|  |         name: 'my_feature_b', | ||||||
|  |         type: 'experimental', | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const { body } = await filterFeaturesByCreatedBy('IS:1'); | ||||||
|  | 
 | ||||||
|  |     expect(body).toMatchObject({ | ||||||
|  |         features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const { body: emptyResults } = await filterFeaturesByCreatedBy('IS:2'); | ||||||
|  | 
 | ||||||
|  |     expect(emptyResults).toMatchObject({ | ||||||
|  |         features: [], | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('should filter features by tag', async () => { | test('should filter features by tag', async () => { | ||||||
|     await app.createFeature('my_feature_a'); |     await app.createFeature('my_feature_a'); | ||||||
|     await app.addTag('my_feature_a', { |     await app.addTag('my_feature_a', { | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ export interface IFeatureSearchParams { | |||||||
|     project?: string; |     project?: string; | ||||||
|     segment?: string; |     segment?: string; | ||||||
|     createdAt?: string; |     createdAt?: string; | ||||||
|  |     createdBy?: string; | ||||||
|     state?: string; |     state?: string; | ||||||
|     type?: string; |     type?: string; | ||||||
|     tag?: string; |     tag?: string; | ||||||
|  | |||||||
| @ -46,6 +46,18 @@ export const featureSearchQueryParameters = [ | |||||||
|             'The feature flag type to filter by. The type can be specified with an operator. The supported operators are IS, IS_NOT, IS_ANY_OF, IS_NONE_OF.', |             'The feature flag type to filter by. The type can be specified with an operator. The supported operators are IS, IS_NOT, IS_ANY_OF, IS_NONE_OF.', | ||||||
|         in: 'query', |         in: 'query', | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |         name: 'createdBy', | ||||||
|  |         schema: { | ||||||
|  |             type: 'string', | ||||||
|  |             example: 'IS:1', | ||||||
|  |             pattern: | ||||||
|  |                 '^(IS|IS_NOT|IS_ANY_OF|IS_NONE_OF):(.*?)(,([a-zA-Z0-9_]+))*$', | ||||||
|  |         }, | ||||||
|  |         description: | ||||||
|  |             'The feature flag creator to filter by. The creators can be specified with an operator. The supported operators are IS, IS_NOT, IS_ANY_OF, IS_NONE_OF.', | ||||||
|  |         in: 'query', | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|         name: 'tag', |         name: 'tag', | ||||||
|         schema: { |         schema: { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user