Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: kept and discarded read model #7045

Merged
merged 2 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export const populateCurrentStage = (
case 'completed':
return {
name: 'completed',
status: 'kept',
status:
feature.lifecycle.status === 'discarded'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

special case for discarded, other states use green icon

? 'discarded'
: 'kept',
environments: getFilteredEnvironments(() => true),
enteredStageAt,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ const FeatureOverviewMetaData = () => {
<span>{project}</span>
</SpacedBodyItem>
<ConditionallyRender
condition={featureLifecycleEnabled}
condition={
featureLifecycleEnabled &&
Boolean(feature.lifecycle)
}
show={
<SpacedBodyItem data-loading>
<StyledLabel>Lifecycle:</StyledLabel>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/featureToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type ILastSeenEnvironments = Pick<

export type Lifecycle = {
stage: 'initial' | 'pre-live' | 'live' | 'completed' | 'archived';
status?: string;
enteredStageAt: string;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
...existingStages,
{
stage: featureLifecycleStage.stage,
...(featureLifecycleStage.status
? { status: featureLifecycleStage.status }
: {}),
enteredStageAt: new Date(),
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { IFeatureLifecycleStage, StageName } from '../../types';
type DBType = {
feature: string;
stage: StageName;
status: string | null;
created_at: Date;
};

Expand All @@ -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,
}));

Expand Down
7 changes: 4 additions & 3 deletions src/lib/features/feature-lifecycle/feature-lifecycle-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type { StageName } from '../../types';
type DBType = {
stage: StageName;
created_at: string;
status?: string;
status_value?: string;
status: string | null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in DB the column is nullable

status_value: string | null;
};

type DBProjectType = DBType & {
Expand Down Expand Up @@ -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),
}));
}
Expand Down
11 changes: 11 additions & 0 deletions src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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`)
Expand All @@ -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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added new method to test the read model directly inside this module

return featureLifecycleReadModel.findCurrentStage(featureName);
};

const completeFeature = async (
featureName: string,
status: FeatureLifecycleCompletedSchema,
Expand Down Expand Up @@ -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');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('getCurrentStage', () => {
},
{
stage: 'completed',
status: 'kept',
enteredStageAt: irrelevantDate,
},
{
Expand Down
4 changes: 4 additions & 0 deletions src/lib/features/feature-search/feature-search-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/features/feature-search/feature.search.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/lib/openapi/spec/feature-lifecycle-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/lib/openapi/spec/feature-search-response-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export type StageName =
export interface IFeatureLifecycleStage {
stage: StageName;
enteredStageAt: Date;
status?: string;
}

export type IProjectLifecycleStageDuration = {
Expand Down
Loading