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:
parent
46d7cb236d
commit
1c8fab63e2
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user