From 8f117ac18e6ebf019a2af5db36052f682e135072 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 10 Apr 2025 10:25:39 +0200 Subject: [PATCH] feat: add milestones to search results (#9739) --- .../feature-search/feature-search-store.ts | 54 +++++++++++ .../feature-search/feature.search.e2e.test.ts | 90 +++++++++++++++++++ .../spec/feature-environment-schema.ts | 18 ++++ src/server-dev.ts | 1 + 4 files changed, 163 insertions(+) diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 4d0459b1ab..cd49ec67f5 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -76,6 +76,13 @@ class FeatureSearchStore implements IFeatureSearchStore { yes: Number(r.yes) || 0, no: Number(r.no) || 0, changeRequestIds: r.change_request_ids ?? [], + ...(r.milestone_name + ? { + milestoneName: r.milestone_name, + milestoneOrder: r.milestone_order, + totalMilestones: Number(r.total_milestones || 0), + } + : {}), }; } @@ -371,6 +378,53 @@ class FeatureSearchStore implements IFeatureSearchStore { }, ) .select('feature_cr.change_request_ids'); + + finalQuery + .leftJoin( + this.db + .with('total_milestones', (qb) => { + qb.select('release_plan_definition_id') + .count('* as total_milestones') + .from('milestones') + .groupBy('release_plan_definition_id'); + }) + .select([ + 'rpd.feature_name', + 'rpd.environment', + 'active_milestone.sort_order AS milestone_order', + 'total_milestones.total_milestones', + 'active_milestone.name AS milestone_name', + ]) + .from('release_plan_definitions AS rpd') + .join( + 'total_milestones', + 'total_milestones.release_plan_definition_id', + 'rpd.id', + ) + .join( + 'milestones AS active_milestone', + 'active_milestone.id', + 'rpd.active_milestone_id', + ) + .where('rpd.discriminator', 'plan') + .as('feature_release_plan'), + function () { + this.on( + 'feature_release_plan.feature_name', + '=', + 'ranked_features.feature_name', + ).andOn( + 'feature_release_plan.environment', + '=', + 'ranked_features.environment', + ); + }, + ) + .select([ + 'feature_release_plan.milestone_name', + 'feature_release_plan.milestone_order', + 'feature_release_plan.total_milestones', + ]); } this.queryExtraData(finalQuery); const rows = await finalQuery; 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 d8194e0fc4..20df3ea864 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -1425,3 +1425,93 @@ test('should return change request ids per environment', async () => { ], }); }); + +const createReleasePlan = async ({ + feature, + environment, + planId, +}: { feature: string; environment: string; planId: string }) => { + await db.rawDatabase('release_plan_definitions').insert({ + id: planId, + discriminator: 'plan', + name: 'plan', + feature_name: feature, + environment: environment, + created_by_user_id: 1, + }); +}; + +const createMilestone = async ({ + id, + name, + order, + planId, +}: { id: string; name: string; order: number; planId: string }) => { + await db.rawDatabase('milestones').insert({ + id, + name, + sort_order: order, + release_plan_definition_id: planId, + }); +}; + +const activateMilestone = async ({ + planId, + milestoneId, +}: { planId: string; milestoneId: string }) => { + await db + .rawDatabase('release_plan_definitions') + .update({ active_milestone_id: milestoneId }) + .where('id', planId); +}; + +test('should return release plan milestones', async () => { + await app.createFeature('my_feature_a'); + + await createReleasePlan({ + feature: 'my_feature_a', + environment: 'development', + planId: 'plan0', + }); + await createMilestone({ + id: 'milestone0', + name: 'Milestone 1', + order: 0, + planId: 'plan0', + }); + await createMilestone({ + id: 'milestone1', + name: 'Milestone 2', + order: 1, + planId: 'plan0', + }); + await createMilestone({ + id: 'milestone3', + name: 'Milestone 3', + order: 2, + planId: 'plan0', + }); + await activateMilestone({ planId: 'plan0', milestoneId: 'milestone1' }); + + const { body } = await searchFeatures({}); + + expect(body).toMatchObject({ + features: [ + { + name: 'my_feature_a', + environments: [ + { name: 'default' }, + { + name: 'development', + totalMilestones: 3, + milestoneName: 'Milestone 2', + milestoneOrder: 1, + }, + { name: 'production' }, + ], + }, + ], + }); + expect(body.features[0].environments[0].milestoneName).toBeUndefined(); + expect(body.features[0].environments[2].milestoneName).toBeUndefined(); +}); diff --git a/src/lib/openapi/spec/feature-environment-schema.ts b/src/lib/openapi/spec/feature-environment-schema.ts index aac5d48fec..78c33c9ce0 100644 --- a/src/lib/openapi/spec/feature-environment-schema.ts +++ b/src/lib/openapi/spec/feature-environment-schema.ts @@ -71,6 +71,24 @@ export const featureEnvironmentSchema = { description: 'Experimental. A list of change request identifiers for actionable change requests that are not Cancelled, Rejected or Approved.', }, + milestoneName: { + type: 'string', + example: 'Phase One', + description: + 'Experimental: The name of the currently active release plan milestone', + }, + milestoneOrder: { + type: 'number', + example: 0, + description: + 'Experimental: The zero-indexed order of currently active milestone in the list of all release plan milestones', + }, + totalMilestones: { + type: 'number', + example: 0, + description: + 'Experimental: The total number of milestones in the feature environment release plan', + }, lastSeenAt: { type: 'string', format: 'date-time', diff --git a/src/server-dev.ts b/src/server-dev.ts index 300ffd303c..dd045efbba 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -60,6 +60,7 @@ process.nextTick(async () => { tagTypeColor: true, newStrategyDropdown: true, addEditStrategy: true, + flagsOverviewSearch: true, }, }, authentication: {