1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

feat: filter by created by (#7306)

This commit is contained in:
Mateusz Kwasniewski 2024-06-06 12:59:11 +02:00 committed by GitHub
parent bb3498adb6
commit a91b77a7ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 4 deletions

View File

@ -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}
/> />

View File

@ -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

View File

@ -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}`,

View File

@ -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 };
};

View File

@ -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,

View File

@ -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',

View File

@ -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', {

View File

@ -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;

View File

@ -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: {