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