mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-27 00:19:39 +01:00
feat: start returning onboarding status with project overview (#8058)
To show/hide onboarding flow, we need to get extra info about onboarding status. This PR adds it to project overview.
This commit is contained in:
parent
c865fd4fbb
commit
037651c35f
@ -52,8 +52,8 @@ import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-res
|
||||
import { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
|
||||
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
|
||||
import { createProjectReadModel } from '../features/project/createProjectReadModel';
|
||||
import { OnboardingReadModel } from '../features/onboarding/onboarding-read-model';
|
||||
import { OnboardingStore } from '../features/onboarding/onboarding-store';
|
||||
import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';
|
||||
|
||||
export const createStores = (
|
||||
config: IUnleashConfig,
|
||||
@ -173,7 +173,7 @@ export const createStores = (
|
||||
projectFlagCreatorsReadModel: new ProjectFlagCreatorsReadModel(db),
|
||||
featureLifecycleStore: new FeatureLifecycleStore(db),
|
||||
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
|
||||
onboardingReadModel: new OnboardingReadModel(db),
|
||||
onboardingReadModel: createOnboardingReadModel(db),
|
||||
onboardingStore: new OnboardingStore(db),
|
||||
featureLifecycleReadModel: new FeatureLifecycleReadModel(
|
||||
db,
|
||||
|
12
src/lib/features/onboarding/createOnboardingReadModel.ts
Normal file
12
src/lib/features/onboarding/createOnboardingReadModel.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { Db } from '../../server-impl';
|
||||
import type { IOnboardingReadModel } from '../../types';
|
||||
import { OnboardingReadModel } from './onboarding-read-model';
|
||||
import { FakeOnboardingReadModel } from './fake-onboarding-read-model';
|
||||
|
||||
export const createOnboardingReadModel = (db: Db): IOnboardingReadModel => {
|
||||
return new OnboardingReadModel(db);
|
||||
};
|
||||
|
||||
export const createFakeOnboardingReadModel = (): IOnboardingReadModel => {
|
||||
return new FakeOnboardingReadModel();
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import type { IOnboardingReadModel } from '../../types';
|
||||
import type {
|
||||
InstanceOnboarding,
|
||||
OnboardingStatus,
|
||||
ProjectOnboarding,
|
||||
} from './onboarding-read-model-type';
|
||||
|
||||
@ -17,4 +18,10 @@ export class FakeOnboardingReadModel implements IOnboardingReadModel {
|
||||
getProjectsOnboardingMetrics(): Promise<ProjectOnboarding[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getOnboardingStatusForProject(
|
||||
projectId: string,
|
||||
): Promise<OnboardingStatus> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import type { ProjectOverviewSchema } from '../../openapi';
|
||||
|
||||
export type OnboardingStatus = ProjectOverviewSchema['onboardingStatus'];
|
||||
|
||||
/**
|
||||
* All the values are in minutes
|
||||
* All the values are in seconds
|
||||
*/
|
||||
export type InstanceOnboarding = {
|
||||
firstLogin: number | null;
|
||||
@ -10,7 +14,7 @@ export type InstanceOnboarding = {
|
||||
};
|
||||
|
||||
/**
|
||||
* All the values are in minutes
|
||||
* All the values are in seconds
|
||||
*/
|
||||
export type ProjectOnboarding = {
|
||||
project: string;
|
||||
@ -22,4 +26,5 @@ export type ProjectOnboarding = {
|
||||
export interface IOnboardingReadModel {
|
||||
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding>;
|
||||
getProjectsOnboardingMetrics(): Promise<Array<ProjectOnboarding>>;
|
||||
getOnboardingStatusForProject(projectId: string): Promise<OnboardingStatus>;
|
||||
}
|
||||
|
@ -1,19 +1,27 @@
|
||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import type { IOnboardingStore } from '../../types';
|
||||
import { OnboardingReadModel } from './onboarding-read-model';
|
||||
import {
|
||||
type IFeatureToggleStore,
|
||||
type ILastSeenStore,
|
||||
type IOnboardingStore,
|
||||
SYSTEM_USER,
|
||||
} from '../../types';
|
||||
import type { IOnboardingReadModel } from './onboarding-read-model-type';
|
||||
|
||||
let db: ITestDb;
|
||||
let onboardingReadModel: IOnboardingReadModel;
|
||||
let onBoardingStore: IOnboardingStore;
|
||||
let featureToggleStore: IFeatureToggleStore;
|
||||
let lastSeenStore: ILastSeenStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('onboarding_read_model', getLogger, {
|
||||
experimental: { flags: { onboardingMetrics: true } },
|
||||
});
|
||||
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
|
||||
onboardingReadModel = db.stores.onboardingReadModel;
|
||||
onBoardingStore = db.stores.onboardingStore;
|
||||
featureToggleStore = db.stores.featureToggleStore;
|
||||
lastSeenStore = db.stores.lastSeenStore;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -109,3 +117,39 @@ test('can get instance onboarding durations', async () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('can get project onboarding status', async () => {
|
||||
const onboardingStartedResult =
|
||||
await onboardingReadModel.getOnboardingStatusForProject('default');
|
||||
|
||||
expect(onboardingStartedResult).toMatchObject({
|
||||
status: 'onboarding-started',
|
||||
});
|
||||
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'my-flag',
|
||||
createdByUserId: SYSTEM_USER.id,
|
||||
});
|
||||
|
||||
const firstFlagResult =
|
||||
await onboardingReadModel.getOnboardingStatusForProject('default');
|
||||
|
||||
expect(firstFlagResult).toMatchObject({
|
||||
status: 'first-flag-created',
|
||||
feature: 'my-flag',
|
||||
});
|
||||
|
||||
await lastSeenStore.setLastSeen([
|
||||
{
|
||||
environment: 'default',
|
||||
featureName: 'my-flag',
|
||||
},
|
||||
]);
|
||||
|
||||
const onboardedResult =
|
||||
await onboardingReadModel.getOnboardingStatusForProject('default');
|
||||
|
||||
expect(onboardedResult).toMatchObject({
|
||||
status: 'onboarded',
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import type {
|
||||
IOnboardingReadModel,
|
||||
InstanceOnboarding,
|
||||
ProjectOnboarding,
|
||||
OnboardingStatus,
|
||||
} from './onboarding-read-model-type';
|
||||
|
||||
const instanceEventLookup = {
|
||||
@ -78,4 +79,30 @@ export class OnboardingReadModel implements IOnboardingReadModel {
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getOnboardingStatusForProject(
|
||||
projectId: string,
|
||||
): Promise<OnboardingStatus> {
|
||||
const feature = await this.db('features')
|
||||
.select('name')
|
||||
.where('project', projectId)
|
||||
.first();
|
||||
|
||||
if (!feature) {
|
||||
return { status: 'onboarding-started' };
|
||||
}
|
||||
|
||||
const lastSeen = await this.db('last_seen_at_metrics as lsm')
|
||||
.select('lsm.feature_name')
|
||||
.innerJoin('features as f', 'f.name', 'lsm.feature_name')
|
||||
.innerJoin('projects as p', 'p.id', 'f.project')
|
||||
.where('p.id', projectId)
|
||||
.first();
|
||||
|
||||
if (lastSeen) {
|
||||
return { status: 'onboarded' };
|
||||
} else {
|
||||
return { status: 'first-flag-created', feature: feature.name };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { createOnboardingService } from './createOnboardingService';
|
||||
import type EventEmitter from 'events';
|
||||
import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events';
|
||||
import { OnboardingReadModel } from './onboarding-read-model';
|
||||
|
||||
let db: ITestDb;
|
||||
let stores: IUnleashStores;
|
||||
@ -24,7 +23,7 @@ beforeAll(async () => {
|
||||
eventBus = config.eventBus;
|
||||
onboardingService = createOnboardingService(config)(db.rawDatabase);
|
||||
onboardingService.listen();
|
||||
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
|
||||
onboardingReadModel = db.stores.onboardingReadModel;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -52,6 +52,10 @@ import {
|
||||
createFakeProjectReadModel,
|
||||
createProjectReadModel,
|
||||
} from './createProjectReadModel';
|
||||
import {
|
||||
createFakeOnboardingReadModel,
|
||||
createOnboardingReadModel,
|
||||
} from '../onboarding/createOnboardingReadModel';
|
||||
|
||||
export const createProjectService = (
|
||||
db: Db,
|
||||
@ -130,6 +134,8 @@ export const createProjectService = (
|
||||
config.flagResolver,
|
||||
);
|
||||
|
||||
const onboardingReadModel = createOnboardingReadModel(db);
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
@ -142,6 +148,7 @@ export const createProjectService = (
|
||||
projectOwnersReadModel,
|
||||
projectFlagCreatorsReadModel,
|
||||
projectReadModel,
|
||||
onboardingReadModel,
|
||||
},
|
||||
config,
|
||||
accessService,
|
||||
@ -197,6 +204,8 @@ export const createFakeProjectService = (
|
||||
|
||||
const projectReadModel = createFakeProjectReadModel();
|
||||
|
||||
const onboardingReadModel = createFakeOnboardingReadModel();
|
||||
|
||||
return new ProjectService(
|
||||
{
|
||||
projectStore,
|
||||
@ -209,6 +218,7 @@ export const createFakeProjectService = (
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
projectReadModel,
|
||||
onboardingReadModel,
|
||||
},
|
||||
config,
|
||||
accessService,
|
||||
|
@ -54,6 +54,7 @@ import {
|
||||
RoleName,
|
||||
SYSTEM_USER_ID,
|
||||
type IProjectReadModel,
|
||||
type IOnboardingReadModel,
|
||||
} from '../../types';
|
||||
import type {
|
||||
IProjectAccessModel,
|
||||
@ -164,6 +165,8 @@ export default class ProjectService {
|
||||
|
||||
private projectReadModel: IProjectReadModel;
|
||||
|
||||
private onboardingReadModel: IOnboardingReadModel;
|
||||
|
||||
constructor(
|
||||
{
|
||||
projectStore,
|
||||
@ -176,6 +179,7 @@ export default class ProjectService {
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
projectReadModel,
|
||||
onboardingReadModel,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
@ -188,6 +192,7 @@ export default class ProjectService {
|
||||
| 'accountStore'
|
||||
| 'projectStatsStore'
|
||||
| 'projectReadModel'
|
||||
| 'onboardingReadModel'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
@ -220,6 +225,7 @@ export default class ProjectService {
|
||||
this.resourceLimits = config.resourceLimits;
|
||||
this.eventBus = config.eventBus;
|
||||
this.projectReadModel = projectReadModel;
|
||||
this.onboardingReadModel = onboardingReadModel;
|
||||
}
|
||||
|
||||
async getProjects(
|
||||
@ -1491,6 +1497,7 @@ export default class ProjectService {
|
||||
members,
|
||||
favorite,
|
||||
projectStats,
|
||||
onboardingStatus,
|
||||
] = await Promise.all([
|
||||
this.projectStore.get(projectId),
|
||||
this.projectStore.getEnvironmentsForProject(projectId),
|
||||
@ -1507,6 +1514,7 @@ export default class ProjectService {
|
||||
})
|
||||
: Promise.resolve(false),
|
||||
this.projectStatsStore.getProjectStats(projectId),
|
||||
this.onboardingReadModel.getOnboardingStatusForProject(projectId),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -1524,6 +1532,7 @@ export default class ProjectService {
|
||||
? { archivedAt: project.archivedAt }
|
||||
: {}),
|
||||
createdAt: project.createdAt,
|
||||
onboardingStatus,
|
||||
environments,
|
||||
featureTypeCounts,
|
||||
members,
|
||||
|
@ -16,6 +16,9 @@ test('projectOverviewSchema', () => {
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
onboardingStatus: {
|
||||
status: 'onboarding-started',
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
|
@ -19,7 +19,7 @@ export const projectOverviewSchema = {
|
||||
$id: '#/components/schemas/projectOverviewSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['version', 'name'],
|
||||
required: ['version', 'name', 'onboardingStatus'],
|
||||
description:
|
||||
'A high-level overview of a project. It contains information such as project statistics, the name of the project, what members and what features it contains, etc.',
|
||||
properties: {
|
||||
@ -135,6 +135,41 @@ export const projectOverviewSchema = {
|
||||
description:
|
||||
'`true` if the project was favorited, otherwise `false`.',
|
||||
},
|
||||
onboardingStatus: {
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['onboarding-started', 'onboarded'],
|
||||
example: 'onboarding-started',
|
||||
},
|
||||
},
|
||||
required: ['status'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['first-flag-created'],
|
||||
example: 'first-flag-created',
|
||||
},
|
||||
feature: {
|
||||
type: 'string',
|
||||
description: 'The name of the feature flag',
|
||||
example: 'my-feature-flag',
|
||||
},
|
||||
},
|
||||
required: ['status', 'feature'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
description: 'The current onboarding status of the project.',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
|
@ -4,7 +4,10 @@ import type { IRole } from './stores/access-store';
|
||||
import type { IUser } from './user';
|
||||
import type { ALL_OPERATORS } from '../util';
|
||||
import type { IProjectStats } from '../features/project/project-service';
|
||||
import type { CreateFeatureStrategySchema } from '../openapi';
|
||||
import type {
|
||||
CreateFeatureStrategySchema,
|
||||
ProjectOverviewSchema,
|
||||
} from '../openapi';
|
||||
import type { ProjectEnvironment } from '../features/project/project-store-type';
|
||||
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
||||
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||
@ -313,6 +316,7 @@ export interface IProjectOverview {
|
||||
featureLimit?: number;
|
||||
featureNaming?: IFeatureNaming;
|
||||
defaultStickiness: string;
|
||||
onboardingStatus: ProjectOverviewSchema['onboardingStatus'];
|
||||
}
|
||||
|
||||
export interface IProjectHealthReport extends IProjectHealth {
|
||||
|
4
src/test/fixtures/store.ts
vendored
4
src/test/fixtures/store.ts
vendored
@ -52,8 +52,8 @@ import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecy
|
||||
import { FakeLargestResourcesReadModel } from '../../lib/features/metrics/sizes/fake-largest-resources-read-model';
|
||||
import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-toggle/fake-feature-collaborators-read-model';
|
||||
import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
|
||||
import { FakeOnboardingReadModel } from '../../lib/features/onboarding/fake-onboarding-read-model';
|
||||
import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store';
|
||||
import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel';
|
||||
|
||||
const db = {
|
||||
select: () => ({
|
||||
@ -111,7 +111,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
featureLifecycleStore: new FakeFeatureLifecycleStore(),
|
||||
featureStrategiesReadModel: new FakeFeatureStrategiesReadModel(),
|
||||
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
|
||||
onboardingReadModel: new FakeOnboardingReadModel(),
|
||||
onboardingReadModel: createFakeOnboardingReadModel(),
|
||||
largestResourcesReadModel: new FakeLargestResourcesReadModel(),
|
||||
integrationEventsStore: {} as IntegrationEventsStore,
|
||||
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
|
||||
|
Loading…
Reference in New Issue
Block a user