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:
parent
bb3498adb6
commit
a91b77a7ce
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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}`,
|
||||
|
@ -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,
|
||||
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,
|
||||
|
@ -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',
|
||||
|
@ -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', {
|
||||
|
@ -26,6 +26,7 @@ export interface IFeatureSearchParams {
|
||||
project?: string;
|
||||
segment?: string;
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
state?: string;
|
||||
type?: 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.',
|
||||
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: {
|
||||
|
Loading…
Reference in New Issue
Block a user