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:
parent
4241e36819
commit
dfc065500d
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ describe('getCurrentStage', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
stage: 'completed',
|
stage: 'completed',
|
||||||
|
status: 'kept',
|
||||||
enteredStageAt: irrelevantDate,
|
enteredStageAt: irrelevantDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user