1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-02 01:17:58 +02:00

feat: kept and discarded read model (#7045)

This commit is contained in:
Mateusz Kwasniewski 2024-05-13 14:24:31 +02:00 committed by GitHub
parent 4241e36819
commit dfc065500d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 51 additions and 8 deletions

View File

@ -43,7 +43,10 @@ export const populateCurrentStage = (
case 'completed': case 'completed':
return { return {
name: 'completed', name: 'completed',
status: 'kept', status:
feature.lifecycle.status === 'discarded'
? 'discarded'
: 'kept',
environments: getFilteredEnvironments(() => true), environments: getFilteredEnvironments(() => true),
enteredStageAt, enteredStageAt,
}; };

View File

@ -137,7 +137,10 @@ const FeatureOverviewMetaData = () => {
<span>{project}</span> <span>{project}</span>
</SpacedBodyItem> </SpacedBodyItem>
<ConditionallyRender <ConditionallyRender
condition={featureLifecycleEnabled} condition={
featureLifecycleEnabled &&
Boolean(feature.lifecycle)
}
show={ show={
<SpacedBodyItem data-loading> <SpacedBodyItem data-loading>
<StyledLabel>Lifecycle:</StyledLabel> <StyledLabel>Lifecycle:</StyledLabel>

View File

@ -34,6 +34,7 @@ export type ILastSeenEnvironments = Pick<
export type Lifecycle = { export type Lifecycle = {
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived'; stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
status?: string;
enteredStageAt: string; enteredStageAt: string;
}; };

View File

@ -27,6 +27,9 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
...existingStages, ...existingStages,
{ {
stage: featureLifecycleStage.stage, stage: featureLifecycleStage.stage,
...(featureLifecycleStage.status
? { status: featureLifecycleStage.status }
: {}),
enteredStageAt: new Date(), enteredStageAt: new Date(),
}, },
]; ];

View File

@ -6,6 +6,7 @@ import type { IFeatureLifecycleStage, StageName } from '../../types';
type DBType = { type DBType = {
feature: string; feature: string;
stage: StageName; stage: StageName;
status: string | null;
created_at: Date; created_at: Date;
}; };
@ -23,8 +24,9 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
.where({ feature }) .where({ feature })
.orderBy('created_at', 'asc'); .orderBy('created_at', 'asc');
const stages = results.map(({ stage, created_at }: DBType) => ({ const stages = results.map(({ stage, status, created_at }: DBType) => ({
stage, stage,
...(status ? { status } : {}),
enteredStageAt: created_at, enteredStageAt: created_at,
})); }));

View File

@ -10,8 +10,8 @@ import type { StageName } from '../../types';
type DBType = { type DBType = {
stage: StageName; stage: StageName;
created_at: string; created_at: string;
status?: string; status: string | null;
status_value?: string; status_value: string | null;
}; };
type DBProjectType = DBType & { type DBProjectType = DBType & {
@ -64,8 +64,9 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
.where({ feature }) .where({ feature })
.orderBy('created_at', 'asc'); .orderBy('created_at', 'asc');
return results.map(({ stage, created_at }: DBType) => ({ return results.map(({ stage, status, created_at }: DBType) => ({
stage, stage,
...(status ? { status } : {}),
enteredStageAt: new Date(created_at), enteredStageAt: new Date(created_at),
})); }));
} }

View File

@ -18,12 +18,15 @@ import {
STAGE_ENTERED, STAGE_ENTERED,
} from './feature-lifecycle-service'; } from './feature-lifecycle-service';
import type { FeatureLifecycleCompletedSchema } from '../../openapi'; 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 app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
let featureLifecycleService: FeatureLifecycleService; let featureLifecycleService: FeatureLifecycleService;
let eventStore: IEventStore; let eventStore: IEventStore;
let eventBus: EventEmitter; let eventBus: EventEmitter;
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('feature_lifecycle', getLogger); db = await dbInit('feature_lifecycle', getLogger);
@ -41,6 +44,7 @@ beforeAll(async () => {
eventStore = db.stores.eventStore; eventStore = db.stores.eventStore;
eventBus = app.config.eventBus; eventBus = app.config.eventBus;
featureLifecycleService = app.services.featureLifecycleService; featureLifecycleService = app.services.featureLifecycleService;
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
await app.request await app.request
.post(`/auth/demo/login`) .post(`/auth/demo/login`)
@ -62,6 +66,11 @@ const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
.get(`/api/admin/projects/default/features/${featureName}/lifecycle`) .get(`/api/admin/projects/default/features/${featureName}/lifecycle`)
.expect(expectedCode); .expect(expectedCode);
}; };
const getCurrentStage = async (featureName: string) => {
return featureLifecycleReadModel.findCurrentStage(featureName);
};
const completeFeature = async ( const completeFeature = async (
featureName: string, featureName: string,
status: FeatureLifecycleCompletedSchema, status: FeatureLifecycleCompletedSchema,
@ -166,6 +175,8 @@ test('should be able to toggle between completed/uncompleted', async () => {
status: 'kept', status: 'kept',
statusValue: 'variant1', statusValue: 'variant1',
}); });
const currentStage = await getCurrentStage('my_feature_b');
expect(currentStage).toMatchObject({ stage: 'completed', status: 'kept' });
await expectFeatureStage('my_feature_b', 'completed'); await expectFeatureStage('my_feature_b', 'completed');

View File

@ -12,6 +12,7 @@ describe('getCurrentStage', () => {
}, },
{ {
stage: 'completed', stage: 'completed',
status: 'kept',
enteredStageAt: irrelevantDate, enteredStageAt: irrelevantDate,
}, },
{ {

View File

@ -79,6 +79,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
.select( .select(
'feature as stage_feature', 'feature as stage_feature',
'stage as latest_stage', 'stage as latest_stage',
'status as stage_status',
'created_at as entered_stage_at', 'created_at as entered_stage_at',
) )
.distinctOn('stage_feature') .distinctOn('stage_feature')
@ -416,6 +417,9 @@ class FeatureSearchStore implements IFeatureSearchStore {
entry.lifecycle = row.latest_stage entry.lifecycle = row.latest_stage
? { ? {
stage: row.latest_stage, stage: row.latest_stage,
...(row.stage_status
? { status: row.stage_status }
: {}),
enteredStageAt: row.entered_stage_at, enteredStageAt: row.entered_stage_at,
} }
: undefined; : undefined;

View File

@ -962,7 +962,7 @@ test('should return environment usage metrics and lifecycle', async () => {
{ feature: 'my_feature_b', stage: 'initial' }, { feature: 'my_feature_b', stage: 'initial' },
]); ]);
await stores.featureLifecycleStore.insert([ await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_b', stage: 'pre-live' }, { feature: 'my_feature_b', stage: 'completed', status: 'discarded' },
]); ]);
const { body } = await searchFeatures({ const { body } = await searchFeatures({
@ -972,7 +972,7 @@ test('should return environment usage metrics and lifecycle', async () => {
features: [ features: [
{ {
name: 'my_feature_b', name: 'my_feature_b',
lifecycle: { stage: 'pre-live' }, lifecycle: { stage: 'completed', status: 'discarded' },
environments: [ environments: [
{ {
name: 'default', name: 'default',

View File

@ -16,6 +16,12 @@ export const featureLifecycleSchema = {
description: description:
'The name of the lifecycle stage that got recorded for a given feature', '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: { enteredStageAt: {
type: 'string', type: 'string',
format: 'date-time', format: 'date-time',

View File

@ -161,6 +161,13 @@ export const featureSearchResponseSchema = {
], ],
example: 'initial', 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: { enteredStageAt: {
description: 'When the feature entered this stage', description: 'When the feature entered this stage',
type: 'string', type: 'string',

View File

@ -163,6 +163,7 @@ export type StageName =
export interface IFeatureLifecycleStage { export interface IFeatureLifecycleStage {
stage: StageName; stage: StageName;
enteredStageAt: Date; enteredStageAt: Date;
status?: string;
} }
export type IProjectLifecycleStageDuration = { export type IProjectLifecycleStageDuration = {