1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

feat: total count respect lifecycle filter (#9724)

This commit is contained in:
Mateusz Kwasniewski 2025-04-09 08:54:19 +02:00 committed by GitHub
parent 827b8f274a
commit e876e6438d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 50 deletions

View File

@ -79,24 +79,6 @@ class FeatureSearchStore implements IFeatureSearchStore {
};
}
private getLatestLifecycleStageQuery() {
return this.db('feature_lifecycles')
.select(
'feature as stage_feature',
'stage as latest_stage',
'status as stage_status',
'created_at as entered_stage_at',
)
.distinctOn('stage_feature')
.orderBy([
'stage_feature',
{
column: 'entered_stage_at',
order: 'desc',
},
]);
}
async searchFeatures(
{
userId,
@ -147,6 +129,9 @@ class FeatureSearchStore implements IFeatureSearchStore {
'users.username as user_username',
'users.email as user_email',
'users.image_url as user_image_url',
'lifecycle.latest_stage',
'lifecycle.stage_status',
'lifecycle.entered_stage_at',
] as (string | Raw<any> | Knex.QueryBuilder)[];
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
@ -245,19 +230,48 @@ class FeatureSearchStore implements IFeatureSearchStore {
'users',
'users.id',
'features.created_by_user_id',
)
.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features.name',
);
})
.leftJoin(
this.db
.select(
'feature as stage_feature',
'stage as latest_stage',
'status as stage_status',
'created_at as entered_stage_at',
)
.from('feature_lifecycles')
.distinctOn('feature')
.orderBy([
'feature',
{ column: 'created_at', order: 'desc' },
])
.as('lifecycle'),
'features.name',
'lifecycle.stage_feature',
);
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'environments.name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features.name',
);
});
if (this.flagResolver.isEnabled('flagsOverviewSearch')) {
const parsedLifecycle = lifecycle
? parseSearchOperatorValue(
'lifecycle.latest_stage',
lifecycle,
)
: null;
if (parsedLifecycle) {
applyGenericQueryParams(query, [parsedLifecycle]);
}
}
const rankingSql = this.buildRankingSql(
favoritesFirst,
@ -270,7 +284,6 @@ class FeatureSearchStore implements IFeatureSearchStore {
.select(selectColumns)
.denseRank('rank', this.db.raw(rankingSql));
})
.with('lifecycle', this.getLatestLifecycleStageQuery())
.with(
'final_ranks',
this.db.raw(
@ -321,26 +334,8 @@ class FeatureSearchStore implements IFeatureSearchStore {
.joinRaw('CROSS JOIN total_features')
.whereBetween('final_rank', [offset + 1, offset + limit])
.orderBy('final_rank');
finalQuery
.select(
'lifecycle.latest_stage',
'lifecycle.stage_status',
'lifecycle.entered_stage_at',
)
.leftJoin(
'lifecycle',
'ranked_features.feature_name',
'lifecycle.stage_feature',
);
if (this.flagResolver.isEnabled('flagsOverviewSearch')) {
const parsedLifecycle = lifecycle
? parseSearchOperatorValue('lifecycle.latest_stage', lifecycle)
: null;
if (parsedLifecycle) {
applyGenericQueryParams(finalQuery, [parsedLifecycle]);
}
finalQuery
.leftJoin(
this.db('change_request_events AS cre')

View File

@ -1111,6 +1111,10 @@ test('should return environment usage metrics and lifecycle', async () => {
name: 'my_feature_b',
createdAt: '2023-01-29T15:21:39.975Z',
});
await app.createFeature({
name: 'my_feature_c',
createdAt: '2023-01-29T15:21:39.975Z',
});
await stores.clientMetricsStoreV2.batchInsertMetrics([
{
@ -1142,6 +1146,9 @@ test('should return environment usage metrics and lifecycle', async () => {
await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_b', stage: 'initial' },
]);
await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_c', stage: 'initial' },
]);
await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_b', stage: 'completed', status: 'discarded' },
]);
@ -1150,6 +1157,7 @@ test('should return environment usage metrics and lifecycle', async () => {
query: 'my_feature_b',
});
expect(noExplicitLifecycle).toMatchObject({
total: 1,
features: [
{
name: 'my_feature_b',
@ -1180,14 +1188,17 @@ test('should return environment usage metrics and lifecycle', async () => {
query: 'my_feature_b',
lifecycle: 'IS:initial',
});
expect(noFeaturesWithOtherLifecycle).toMatchObject({ features: [] });
expect(noFeaturesWithOtherLifecycle).toMatchObject({
total: 0,
features: [],
});
const { body: featureWithMatchingLifecycle } =
await searchFeaturesWithLifecycle({
query: 'my_feature_b',
lifecycle: 'IS:completed',
});
expect(featureWithMatchingLifecycle).toMatchObject({
total: 1,
features: [{ name: 'my_feature_b' }],
});
});