diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
index 7dbb5c5868..e7f9c3b4f1 100644
--- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx
@@ -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'
>
diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx
index bb411aec57..fa8b6ff92c 100644
--- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx
+++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOverviewFilters.tsx
@@ -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 = ({
state,
onChange,
+ project,
}) => {
const { tags } = useAllTags();
+ const { flagCreators } = useProjectFlagCreators(project);
const [availableFilters, setAvailableFilters] = useState([]);
+ const flagCreatorEnabled = useUiFlag('flagCreator');
useEffect(() => {
const tagsOptions = (tags || []).map((tag) => ({
@@ -24,6 +30,11 @@ export const ProjectOverviewFilters: VFC = ({
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 = ({
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 (
{
+ 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}`,
diff --git a/frontend/src/hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators.ts b/frontend/src/hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators.ts
new file mode 100644
index 0000000000..6b84cb4bc6
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators.ts
@@ -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(formatApiPath(PATH), () =>
+ fetcher(formatApiPath(PATH), 'Flag creators'),
+ );
+
+ return { flagCreators: data || [], refetch, error };
+};
diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts
index ef1706d541..ea56a4cc33 100644
--- a/src/lib/features/feature-search/feature-search-controller.ts
+++ b/src/lib/features/feature-search/feature-search-controller.ts
@@ -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,
diff --git a/src/lib/features/feature-search/feature-search-service.ts b/src/lib/features/feature-search/feature-search-service.ts
index 2d9ed8a827..d82cb5fecd 100644
--- a/src/lib/features/feature-search/feature-search-service.ts
+++ b/src/lib/features/feature-search/feature-search-service.ts
@@ -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',
diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts
index 5bd2f6c05b..2a71e19237 100644
--- a/src/lib/features/feature-search/feature.search.e2e.test.ts
+++ b/src/lib/features/feature-search/feature.search.e2e.test.ts
@@ -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', {
diff --git a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts
index dd3bc8ccb9..aa01ebb61d 100644
--- a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts
+++ b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts
@@ -26,6 +26,7 @@ export interface IFeatureSearchParams {
project?: string;
segment?: string;
createdAt?: string;
+ createdBy?: string;
state?: string;
type?: string;
tag?: string;
diff --git a/src/lib/openapi/spec/feature-search-query-parameters.ts b/src/lib/openapi/spec/feature-search-query-parameters.ts
index 91f450c03e..4559a731d0 100644
--- a/src/lib/openapi/spec/feature-search-query-parameters.ts
+++ b/src/lib/openapi/spec/feature-search-query-parameters.ts
@@ -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: {