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:
parent
ba758e13c1
commit
71431c7a1d
@ -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,
|
||||
);
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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[]> {
|
||||
|
@ -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> {
|
||||
|
@ -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);
|
||||
|
@ -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 = () => {
|
||||
|
@ -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;
|
@ -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');
|
||||
});
|
||||
|
@ -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[]>;
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user