1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-06 01:15:28 +02:00

feat: filter by environment status (#5165)

This commit is contained in:
Mateusz Kwasniewski 2023-10-27 08:54:03 +02:00 committed by GitHub
parent 46d7cb236d
commit 1c8fab63e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 9 deletions

View File

@ -72,17 +72,25 @@ export default class FeatureSearchController extends Controller {
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
if (this.config.flagResolver.isEnabled('featureSearchAPI')) { if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
const { query, projectId, type, tag } = req.query; const { query, projectId, type, tag, status } = req.query;
const userId = req.user.id; const userId = req.user.id;
const normalizedTag = tag const normalizedTag = tag
?.map((tag) => tag.split(':')) ?.map((tag) => tag.split(':'))
.filter((tag) => tag.length === 2); .filter((tag) => tag.length === 2);
const normalizedStatus = status
?.map((tag) => tag.split(':'))
.filter(
(tag) =>
tag.length === 2 &&
['enabled', 'disabled'].includes(tag[1]),
);
const features = await this.featureSearchService.search({ const features = await this.featureSearchService.search({
query, query,
projectId, projectId,
type, type,
userId, userId,
tag: normalizedTag, tag: normalizedTag,
status: normalizedStatus,
}); });
res.json({ features }); res.json({ features });
} else { } else {

View File

@ -20,13 +20,9 @@ export class FeatureSearchService {
} }
async search(params: IFeatureSearchParams) { async search(params: IFeatureSearchParams) {
const features = await this.featureStrategiesStore.searchFeatures({ const features = await this.featureStrategiesStore.searchFeatures(
projectId: params.projectId, params,
query: params.query, );
userId: params.userId,
type: params.type,
tag: params.tag,
});
return features; return features;
} }

View File

@ -57,6 +57,18 @@ const filterFeaturesByTag = async (tags: string[], expectedCode = 200) => {
.expect(expectedCode); .expect(expectedCode);
}; };
const filterFeaturesByEnvironmentStatus = async (
environmentStatuses: string[],
expectedCode = 200,
) => {
const statuses = environmentStatuses
.map((status) => `status[]=${status}`)
.join('&');
return app.request
.get(`/api/admin/search/features?${statuses}`)
.expect(expectedCode);
};
const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => { const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
return app.request.get(`/api/admin/search/features`).expect(expectedCode); return app.request.get(`/api/admin/search/features`).expect(expectedCode);
}; };
@ -99,6 +111,22 @@ test('should filter features by tag', async () => {
}); });
}); });
test('should filter features by environment status', async () => {
await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b');
await app.enableFeature('my_feature_a', 'default');
const { body } = await filterFeaturesByEnvironmentStatus([
'default:enabled',
'nonexistentEnv:disabled',
'default:wrongStatus',
]);
expect(body).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
});
test('filter with invalid tag should ignore filter', async () => { test('filter with invalid tag should ignore filter', async () => {
await app.createFeature('my_feature_a'); await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b'); await app.createFeature('my_feature_b');

View File

@ -522,6 +522,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
query: queryString, query: queryString,
type, type,
tag, tag,
status,
}: IFeatureSearchParams): Promise<IFeatureOverview[]> { }: IFeatureSearchParams): Promise<IFeatureOverview[]> {
let query = this.db('features'); let query = this.db('features');
if (projectId) { if (projectId) {
@ -553,6 +554,23 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
if (type) { if (type) {
query = query.whereIn('features.type', type); query = query.whereIn('features.type', type);
} }
if (status && status.length > 0) {
query = query.where((builder) => {
for (const [envName, envStatus] of status) {
builder.orWhere(function () {
this.where(
'feature_environments.environment',
envName,
).andWhere(
'feature_environments.enabled',
envStatus === 'enabled' ? true : false,
);
});
}
});
}
query = query query = query
.modify(FeatureToggleStore.filterByArchived, false) .modify(FeatureToggleStore.filterByArchived, false)
.leftJoin( .leftJoin(

View File

@ -27,6 +27,7 @@ export interface IFeatureSearchParams {
projectId?: string; projectId?: string;
type?: string[]; type?: string[];
tag?: string[][]; tag?: string[][];
status?: string[][];
} }
export interface IFeatureStrategiesStore export interface IFeatureStrategiesStore

View File

@ -41,7 +41,20 @@ export const featureSearchQueryParameters = [
}, },
}, },
description: description:
'The list of feature tags to filter by. Feature tag has to specify type and value joined with a colon.', 'The list of feature tags to filter by. Feature tag has to specify a type and a value joined with a colon.',
in: 'query',
},
{
name: 'status',
schema: {
type: 'array',
items: {
type: 'string',
example: 'production:enabled',
},
},
description:
'The list of feature environment status to filter by. Feature environment has to specify a name and a status joined with a colon.',
in: 'query', in: 'query',
}, },
] as const; ] as const;

View File

@ -47,6 +47,13 @@ export interface IUnleashHttpAPI {
expectedResponseCode?: number, expectedResponseCode?: number,
): supertest.Test; ): supertest.Test;
enableFeature(
feature: string,
environment: string,
project?: string,
expectedResponseCode?: number,
): supertest.Test;
getFeatures(name?: string, expectedResponseCode?: number): supertest.Test; getFeatures(name?: string, expectedResponseCode?: number): supertest.Test;
getProjectFeatures( getProjectFeatures(
@ -219,6 +226,19 @@ function httpApis(
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(expectedResponseCode); .expect(expectedResponseCode);
}, },
enableFeature(
feature: string,
environment,
project = 'default',
expectedResponseCode = 200,
): supertest.Test {
return request
.post(
`/api/admin/projects/${project}/features/${feature}/environments/${environment}/on`,
)
.expect(expectedResponseCode);
},
}; };
} }