From dfc065500d49d16205cc456753c113b07aec5114 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 13 May 2024 14:24:31 +0200 Subject: [PATCH] feat: kept and discarded read model (#7045) --- .../FeatureLifecycle/populateCurrentStage.ts | 5 ++++- .../FeatureOverviewMetaData.tsx | 5 ++++- frontend/src/interfaces/featureToggle.ts | 1 + .../feature-lifecycle/fake-feature-lifecycle-store.ts | 3 +++ .../feature-lifecycle/feature-lifecycle-read-model.ts | 4 +++- .../feature-lifecycle/feature-lifecycle-store.ts | 7 ++++--- .../feature-lifecycle/feature-lifecycle.e2e.test.ts | 11 +++++++++++ .../feature-lifecycle/get-current-stage.test.ts | 1 + .../features/feature-search/feature-search-store.ts | 4 ++++ .../feature-search/feature.search.e2e.test.ts | 4 ++-- src/lib/openapi/spec/feature-lifecycle-schema.ts | 6 ++++++ .../openapi/spec/feature-search-response-schema.ts | 7 +++++++ src/lib/types/model.ts | 1 + 13 files changed, 51 insertions(+), 8 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts index 6798f914333..d62dfd7ccb0 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/populateCurrentStage.ts @@ -43,7 +43,10 @@ export const populateCurrentStage = ( case 'completed': return { name: 'completed', - status: 'kept', + status: + feature.lifecycle.status === 'discarded' + ? 'discarded' + : 'kept', environments: getFilteredEnvironments(() => true), enteredStageAt, }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx index c4904320ac7..7d28eedf190 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx @@ -137,7 +137,10 @@ const FeatureOverviewMetaData = () => { {project} Lifecycle: diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 4d5edd72c05..5d3d3d3821e 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -34,6 +34,7 @@ export type ILastSeenEnvironments = Pick< export type Lifecycle = { stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived'; + status?: string; enteredStageAt: string; }; diff --git a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts index 74ff64cf6ae..d15b1f9f492 100644 --- a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts @@ -27,6 +27,9 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { ...existingStages, { stage: featureLifecycleStage.stage, + ...(featureLifecycleStage.status + ? { status: featureLifecycleStage.status } + : {}), enteredStageAt: new Date(), }, ]; diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts index 1f6e5deee8c..a0dff07d00c 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts @@ -6,6 +6,7 @@ import type { IFeatureLifecycleStage, StageName } from '../../types'; type DBType = { feature: string; stage: StageName; + status: string | null; created_at: Date; }; @@ -23,8 +24,9 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { .where({ feature }) .orderBy('created_at', 'asc'); - const stages = results.map(({ stage, created_at }: DBType) => ({ + const stages = results.map(({ stage, status, created_at }: DBType) => ({ stage, + ...(status ? { status } : {}), enteredStageAt: created_at, })); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts index 925e47af08a..89076e7f308 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts @@ -10,8 +10,8 @@ import type { StageName } from '../../types'; type DBType = { stage: StageName; created_at: string; - status?: string; - status_value?: string; + status: string | null; + status_value: string | null; }; type DBProjectType = DBType & { @@ -64,8 +64,9 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { .where({ feature }) .orderBy('created_at', 'asc'); - return results.map(({ stage, created_at }: DBType) => ({ + return results.map(({ stage, status, created_at }: DBType) => ({ stage, + ...(status ? { status } : {}), enteredStageAt: new Date(created_at), })); } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index ec05cbb0ed8..65e207958a2 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -18,12 +18,15 @@ import { STAGE_ENTERED, } from './feature-lifecycle-service'; import type { FeatureLifecycleCompletedSchema } from '../../openapi'; +import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model'; +import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type'; let app: IUnleashTest; let db: ITestDb; let featureLifecycleService: FeatureLifecycleService; let eventStore: IEventStore; let eventBus: EventEmitter; +let featureLifecycleReadModel: IFeatureLifecycleReadModel; beforeAll(async () => { db = await dbInit('feature_lifecycle', getLogger); @@ -41,6 +44,7 @@ beforeAll(async () => { eventStore = db.stores.eventStore; eventBus = app.config.eventBus; featureLifecycleService = app.services.featureLifecycleService; + featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase); await app.request .post(`/auth/demo/login`) @@ -62,6 +66,11 @@ const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => { .get(`/api/admin/projects/default/features/${featureName}/lifecycle`) .expect(expectedCode); }; + +const getCurrentStage = async (featureName: string) => { + return featureLifecycleReadModel.findCurrentStage(featureName); +}; + const completeFeature = async ( featureName: string, status: FeatureLifecycleCompletedSchema, @@ -166,6 +175,8 @@ test('should be able to toggle between completed/uncompleted', async () => { status: 'kept', statusValue: 'variant1', }); + const currentStage = await getCurrentStage('my_feature_b'); + expect(currentStage).toMatchObject({ stage: 'completed', status: 'kept' }); await expectFeatureStage('my_feature_b', 'completed'); diff --git a/src/lib/features/feature-lifecycle/get-current-stage.test.ts b/src/lib/features/feature-lifecycle/get-current-stage.test.ts index d4e9646448f..31bc61154a1 100644 --- a/src/lib/features/feature-lifecycle/get-current-stage.test.ts +++ b/src/lib/features/feature-lifecycle/get-current-stage.test.ts @@ -12,6 +12,7 @@ describe('getCurrentStage', () => { }, { stage: 'completed', + status: 'kept', enteredStageAt: irrelevantDate, }, { diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 40cffd7a3f4..2b7aa9a955b 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -79,6 +79,7 @@ class FeatureSearchStore implements IFeatureSearchStore { .select( 'feature as stage_feature', 'stage as latest_stage', + 'status as stage_status', 'created_at as entered_stage_at', ) .distinctOn('stage_feature') @@ -416,6 +417,9 @@ class FeatureSearchStore implements IFeatureSearchStore { entry.lifecycle = row.latest_stage ? { stage: row.latest_stage, + ...(row.stage_status + ? { status: row.stage_status } + : {}), enteredStageAt: row.entered_stage_at, } : undefined; 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 c303ce06093..33d78ba3fc2 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -962,7 +962,7 @@ test('should return environment usage metrics and lifecycle', async () => { { feature: 'my_feature_b', stage: 'initial' }, ]); await stores.featureLifecycleStore.insert([ - { feature: 'my_feature_b', stage: 'pre-live' }, + { feature: 'my_feature_b', stage: 'completed', status: 'discarded' }, ]); const { body } = await searchFeatures({ @@ -972,7 +972,7 @@ test('should return environment usage metrics and lifecycle', async () => { features: [ { name: 'my_feature_b', - lifecycle: { stage: 'pre-live' }, + lifecycle: { stage: 'completed', status: 'discarded' }, environments: [ { name: 'default', diff --git a/src/lib/openapi/spec/feature-lifecycle-schema.ts b/src/lib/openapi/spec/feature-lifecycle-schema.ts index 7586fc04e9c..88878f8a75e 100644 --- a/src/lib/openapi/spec/feature-lifecycle-schema.ts +++ b/src/lib/openapi/spec/feature-lifecycle-schema.ts @@ -16,6 +16,12 @@ export const featureLifecycleSchema = { description: 'The name of the lifecycle stage that got recorded for a given feature', }, + status: { + type: 'string', + example: 'kept', + description: + 'The name of the detailed status of a given stage. E.g. completed stage can be kept or discarded.', + }, enteredStageAt: { type: 'string', format: 'date-time', diff --git a/src/lib/openapi/spec/feature-search-response-schema.ts b/src/lib/openapi/spec/feature-search-response-schema.ts index b517e331b9d..8c125a6411a 100644 --- a/src/lib/openapi/spec/feature-search-response-schema.ts +++ b/src/lib/openapi/spec/feature-search-response-schema.ts @@ -161,6 +161,13 @@ export const featureSearchResponseSchema = { ], example: 'initial', }, + status: { + type: 'string', + nullable: true, + example: 'kept', + description: + 'The name of the detailed status of a given stage. E.g. completed stage can be kept or discarded.', + }, enteredStageAt: { description: 'When the feature entered this stage', type: 'string', diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 7d088454473..7fcf38e6367 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -163,6 +163,7 @@ export type StageName = export interface IFeatureLifecycleStage { stage: StageName; enteredStageAt: Date; + status?: string; } export type IProjectLifecycleStageDuration = {