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

refactor/last seen at archived (#5102)

Refactor global archive view and project archive view to include last
seen at by environment
This commit is contained in:
Fredrik Strand Oseberg 2023-10-20 13:17:41 +02:00 committed by GitHub
parent ba758e13c1
commit 71431c7a1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 203 additions and 81 deletions

View File

@ -1,11 +1,11 @@
import { Request, Response } from 'express';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types';
import Controller from '../controller';
import Controller from '../../routes/controller';
import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
import FeatureToggleService from '../../features/feature-toggle/feature-toggle-service';
import { IAuthRequest } from '../unleash-types';
import FeatureToggleService from './feature-toggle-service';
import { IAuthRequest } from '../../routes/unleash-types';
import {
featuresSchema,
FeaturesSchema,
@ -140,7 +140,7 @@ export default class ArchiveController extends Controller {
res: Response<FeaturesSchema>,
): Promise<void> {
const { user } = req;
const features = await this.featureService.getMetadataForAllFeatures(
const features = await this.featureService.getAllArchivedFeatures(
true,
user.id,
);
@ -158,7 +158,7 @@ export default class ArchiveController extends Controller {
): Promise<void> {
const { projectId } = req.params;
const features =
await this.featureService.getMetadataForAllFeaturesByProjectId(
await this.featureService.getArchivedFeaturesByProjectId(
true,
projectId,
);

View File

@ -88,7 +88,7 @@ export class FeatureToggleRowConverter {
const newEnvironment = {
name: row.last_seen_at_env,
lastSeenAt: row.env_last_seen_at,
enabled: row.enabled,
enabled: row.enabled || false,
};
feature.environments.push(newEnvironment);
@ -253,4 +253,32 @@ export class FeatureToggleRowConverter {
return this.formatToggles(result);
};
buildArchivedFeatureToggleListFromRows = (
rows: any[],
): IFeatureToggleListItem[] => {
const result = rows.reduce((acc, row) => {
const feature: PartialDeep<IFeatureToggleListItem> =
acc[row.name] ?? {};
feature.name = row.name;
feature.description = row.description;
feature.type = row.type;
feature.project = row.project;
feature.stale = row.stale;
feature.createdAt = row.created_at;
feature.impressionData = row.impression_data;
feature.lastSeenAt = row.last_seen_at;
feature.archivedAt = row.archived_at;
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
this.addLastSeenByEnvironment(feature, row);
}
acc[row.name] = feature;
return acc;
}, {});
return Object.values(result);
};
}

View File

@ -163,6 +163,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
return this.features.filter((feature) => feature.archived !== archived);
}
async getArchivedFeatures(project: string): Promise<FeatureToggle[]> {
return this.features.filter((feature) => feature.archived === true);
}
async getPlaygroundFeatures(
query?: IFeatureToggleQuery,
): Promise<FeatureConfigurationClient[]> {

View File

@ -2032,11 +2032,18 @@ class FeatureToggleService {
);
}
async getMetadataForAllFeatures(
async getAllArchivedFeatures(
archived: boolean,
userId: number,
): Promise<FeatureToggle[]> {
const features = await this.featureToggleStore.getAll({ archived });
let features;
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
features = await this.featureToggleStore.getArchivedFeatures();
} else {
features = await this.featureToggleStore.getAll({ archived });
}
if (this.flagResolver.isEnabled('privateProjects')) {
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(
@ -2053,11 +2060,15 @@ class FeatureToggleService {
return features;
}
async getMetadataForAllFeaturesByProjectId(
async getArchivedFeaturesByProjectId(
archived: boolean,
project: string,
): Promise<FeatureToggle[]> {
return this.featureToggleStore.getAll({ archived, project });
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
return this.featureToggleStore.getArchivedFeatures(project);
} else {
return this.featureToggleStore.getAll({ archived, project });
}
}
async getProjectId(name: string): Promise<string | undefined> {

View File

@ -20,7 +20,6 @@ import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-b
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
import { IFlagResolver } from '../../../lib/types';
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
import FlagResolver from 'lib/util/flag-resolver';
export type EnvironmentFeatureNames = { [key: string]: string[] };
@ -53,6 +52,17 @@ interface VariantDTO {
variants: IVariant[];
}
const commonSelectColumns = [
'features.name as name',
'features.description as description',
'features.type as type',
'features.project as project',
'features.stale as stale',
'features.impression_data as impression_data',
'features.last_seen_at as last_seen_at',
'features.created_at as created_at',
];
const TABLE = 'features';
const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments';
@ -117,7 +127,22 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
}
private getBaseFeatureQuery = (archived: boolean, environment: string) => {
const builder = new FeatureToggleListBuilder(this.db);
const builder = new FeatureToggleListBuilder(this.db, [
...commonSelectColumns,
'fe.variants as variants',
'fe.enabled as enabled',
'fe.environment as environment',
'fs.id as strategy_id',
'fs.strategy_name as strategy_name',
'fs.title as strategy_title',
'fs.disabled as strategy_disabled',
'fs.parameters as parameters',
'fs.constraints as constraints',
'fs.sort_order as sort_order',
'fs.variants as strategy_variants',
'segments.id as segment_id',
'segments.constraints as segment_constraints',
]);
builder
.query('features')
@ -227,9 +252,43 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
.from(TABLE)
.where(rest)
.modify(FeatureToggleStore.filterByArchived, archived);
return rows.map(this.rowToFeature);
}
async getArchivedFeatures(project?: string): Promise<FeatureToggle[]> {
const builder = new FeatureToggleListBuilder(this.db, [
...commonSelectColumns,
'features.archived_at as archived_at',
]);
builder.query('features').withLastSeenByEnvironment();
builder.addSelectColumn(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
);
builder.addSelectColumn(
'last_seen_at_metrics.environment as last_seen_at_env',
);
let rows;
if (project) {
rows = await builder.internalQuery
.select(builder.getSelectColumns())
.where({ project })
.whereNotNull('archived_at');
} else {
rows = await builder.internalQuery
.select(builder.getSelectColumns())
.whereNotNull('archived_at');
}
return this.featureToggleRowConverter.buildArchivedFeatureToggleListFromRows(
rows,
);
}
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
query.whereIn('name', names);

View File

@ -8,31 +8,9 @@ export class FeatureToggleListBuilder {
private selectColumns: (string | Knex.Raw<any>)[];
constructor(db) {
constructor(db, selectColumns) {
this.db = db;
this.selectColumns = [
'features.name as name',
'features.description as description',
'features.type as type',
'features.project as project',
'features.stale as stale',
'features.impression_data as impression_data',
'features.last_seen_at as last_seen_at',
'features.created_at as created_at',
'fe.variants as variants',
'fe.enabled as enabled',
'fe.environment as environment',
'fs.id as strategy_id',
'fs.strategy_name as strategy_name',
'fs.title as strategy_title',
'fs.disabled as strategy_disabled',
'fs.parameters as parameters',
'fs.constraints as constraints',
'fs.sort_order as sort_order',
'fs.variants as strategy_variants',
'segments.id as segment_id',
'segments.constraints as segment_constraints',
] as (string | Knex.Raw<any>)[];
this.selectColumns = selectColumns;
}
getSelectColumns = () => {

View File

@ -1,9 +1,9 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import dbInit, { ITestDb } from '../../../../test/e2e/helpers/database-init';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
} from '../../../../test/e2e/helpers/test-helper';
import getLogger from '../../../../test/fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;

View File

@ -9,6 +9,14 @@ import getLogger from '../../../../test/fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;
const setupLastSeenAtTest = async (featureName: string) => {
await app.createFeature(featureName);
await insertLastSeenAt(featureName, db.rawDatabase, 'default');
await insertLastSeenAt(featureName, db.rawDatabase, 'development');
await insertLastSeenAt(featureName, db.rawDatabase, 'production');
};
beforeAll(async () => {
const config = {
experimental: {
@ -27,6 +35,29 @@ beforeAll(async () => {
config,
);
app = await setupAppWithCustomConfig(db.stores, config, db.rawDatabase);
await db.stores.environmentStore.create({
name: 'development',
type: 'development',
sortOrder: 1,
enabled: true,
});
await db.stores.environmentStore.create({
name: 'production',
type: 'production',
sortOrder: 2,
enabled: true,
});
await app.services.projectService.addEnvironmentToProject(
'default',
'development',
);
await app.services.projectService.addEnvironmentToProject(
'default',
'production',
);
});
afterAll(async () => {
@ -53,48 +84,10 @@ test('should return last seen at per env for /api/admin/features', async () => {
);
});
test('response should include last seen at per environment for multiple environments', async () => {
await db.stores.environmentStore.create({
name: 'development',
type: 'development',
sortOrder: 1,
enabled: true,
});
await db.stores.environmentStore.create({
name: 'production',
type: 'production',
sortOrder: 2,
enabled: true,
});
await app.services.projectService.addEnvironmentToProject(
'default',
'development',
);
await app.services.projectService.addEnvironmentToProject(
'default',
'production',
);
await app.createFeature('multiple-environment-last-seen-at');
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'default',
);
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'development',
);
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'production',
);
test('response should include last seen at per environment for multiple environments in /api/admin/features', async () => {
const featureName = 'multiple-environment-last-seen-at';
await setupLastSeenAtTest(featureName);
const { body } = await app.request
.get('/api/admin/features')
.expect('Content-Type', /json/)
@ -113,3 +106,51 @@ test('response should include last seen at per environment for multiple environm
expect(production.name).toBe('production');
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
});
test('response should include last seen at per environment for multiple environments in /api/admin/archive/features', async () => {
const featureName = 'multiple-environment-last-seen-at-archived';
await setupLastSeenAtTest(featureName);
await app.request
.delete(`/api/admin/projects/default/features/${featureName}`)
.expect(202);
const { body } = await app.request.get(`/api/admin/archive/features`);
const featureEnvironments = body.features[0].environments;
const [def, development, production] = featureEnvironments;
expect(def.name).toBe('default');
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
expect(development.name).toBe('development');
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
expect(production.name).toBe('production');
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
});
test('response should include last seen at per environment for multiple environments in /api/admin/archive/features/:projectId', async () => {
const featureName = 'multiple-environment-last-seen-at-archived-project';
await setupLastSeenAtTest(featureName);
await app.request
.delete(`/api/admin/projects/default/features/${featureName}`)
.expect(202);
const { body } = await app.request.get(
`/api/admin/archive/features/default`,
);
const featureEnvironments = body.features[0].environments;
const [def, development, production] = featureEnvironments;
expect(def.name).toBe('default');
expect(def.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
expect(development.name).toBe('development');
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
expect(production.name).toBe('production');
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
});

View File

@ -37,6 +37,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
userId?: number,
archived?: boolean,
): Promise<FeatureToggle[]>;
getArchivedFeatures(project?: string): Promise<FeatureToggle[]>;
getPlaygroundFeatures(
featureQuery?: IFeatureToggleQuery,
): Promise<FeatureConfigurationClient[]>;

View File

@ -2,7 +2,7 @@ import Controller from '../controller';
import { IUnleashServices, IUnleashConfig } from '../../types';
import FeatureController from '../../features/feature-toggle/legacy/feature-toggle-legacy-controller';
import { FeatureTypeController } from './feature-type';
import ArchiveController from './archive';
import ArchiveController from '../../features/feature-toggle/archive-feature-toggle-controller';
import StrategyController from './strategy';
import EventController from './event';
import PlaygroundController from '../../features/playground/playground';