1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01: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,
}: IPaginatedProjectFeatureTogglesProps) => {
const projectId = useRequiredPathParam('projectId');
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
const flagCreatorEnabled = useUiFlag('flagCreator');
const {
features,
@ -75,6 +77,7 @@ export const ProjectFeatureToggles = ({
tag: tableState.tag,
createdAt: tableState.createdAt,
type: tableState.type,
...(flagCreatorEnabled ? { createdBy: tableState.createdBy } : {}),
};
const { favorite, unfavorite } = useFavoriteFeaturesApi();
@ -101,9 +104,6 @@ export const ProjectFeatureToggles = ({
const isPlaceholder = Boolean(initialLoad || (loading && total));
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
const flagCreatorEnabled = useUiFlag('flagCreator');
const columns = useMemo(
() => [
columnHelper.display({
@ -490,6 +490,7 @@ export const ProjectFeatureToggles = ({
aria-live='polite'
>
<ProjectOverviewFilters
project={projectId}
onChange={setTableState}
state={filterState}
/>

View File

@ -5,18 +5,24 @@ import {
Filters,
type IFilterItem,
} from 'component/filter/Filters/Filters';
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectOverviewFilters {
state: FilterItemParamHolder;
onChange: (value: FilterItemParamHolder) => void;
project: string;
}
export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
state,
onChange,
project,
}) => {
const { tags } = useAllTags();
const { flagCreators } = useProjectFlagCreators(project);
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
const flagCreatorEnabled = useUiFlag('flagCreator');
useEffect(() => {
const tagsOptions = (tags || []).map((tag) => ({
@ -24,6 +30,11 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
value: `${tag.type}:${tag.value}`,
}));
const flagCreatorsOptions = flagCreators.map((creator) => ({
label: creator.name,
value: String(creator.id),
}));
const availableFilters: IFilterItem[] = [
{
label: 'Tags',
@ -60,9 +71,19 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
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);
}, [JSON.stringify(tags)]);
}, [JSON.stringify(tags), JSON.stringify(flagCreators)]);
return (
<Filters

View File

@ -15,6 +15,7 @@ import {
} from 'utils/serializeQueryParams';
import { usePersistentTableState } from 'hooks/usePersistentTableState';
import mapValues from 'lodash.mapvalues';
import { useUiFlag } from 'hooks/useUiFlag';
type Attribute =
| { key: 'tag'; operator: 'INCLUDE' }
@ -25,6 +26,7 @@ export const useProjectFeatureSearch = (
storageKey = 'project-overview-v2',
refreshInterval = 15 * 1000,
) => {
const flagCreatorEnabled = useUiFlag('flagCreator');
const stateConfig = {
offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
@ -36,6 +38,7 @@ export const useProjectFeatureSearch = (
tag: FilterItemParam,
createdAt: FilterItemParam,
type: FilterItemParam,
...(flagCreatorEnabled ? { createdBy: FilterItemParam } : {}),
};
const [tableState, setTableState] = usePersistentTableState(
`${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,
segment,
createdAt,
createdBy,
state,
status,
favoritesFirst,
@ -116,6 +117,7 @@ export default class FeatureSearchController extends Controller {
segment,
state,
createdAt,
createdBy,
status: normalizedStatus,
offset: normalizedOffset,
limit: normalizedLimit,

View File

@ -75,6 +75,14 @@ export class FeatureSearchService {
if (parsed) queryParams.push(parsed);
}
if (params.createdBy) {
const parsed = this.parseOperatorValue(
'users.id',
params.createdBy,
);
if (parsed) queryParams.push(parsed);
}
if (params.type) {
const parsed = this.parseOperatorValue(
'features.type',

View File

@ -108,6 +108,15 @@ const filterFeaturesByType = async (typeParams: string, expectedCode = 200) => {
.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) => {
return app.request
.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 () => {
await app.createFeature('my_feature_a');
await app.addTag('my_feature_a', {

View File

@ -26,6 +26,7 @@ export interface IFeatureSearchParams {
project?: string;
segment?: string;
createdAt?: string;
createdBy?: string;
state?: string;
type?: 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.',
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',
schema: {