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

feat: add last seen filter to feature and project searches

This commit is contained in:
Tymoteusz Czech 2025-07-31 21:28:07 +02:00
parent 2629705501
commit c9df6c370f
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
11 changed files with 118 additions and 1 deletions

View File

@ -105,6 +105,13 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
filterKey: 'createdAt',
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
},
{
label: 'Last seen',
icon: 'visibility',
options: [],
filterKey: 'lastSeenAt',
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
},
{
label: 'Flag type',
icon: 'flag',

View File

@ -31,6 +31,7 @@ export const useGlobalFeatureSearch = (pageLimit = DEFAULT_PAGE_LIMIT) => {
state: FilterItemParam,
segment: FilterItemParam,
createdAt: FilterItemParam,
lastSeenAt: FilterItemParam,
type: FilterItemParam,
lifecycle: FilterItemParam,
createdBy: FilterItemParam,

View File

@ -81,6 +81,13 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
filterKey: 'createdAt',
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
},
{
label: 'Last seen',
icon: 'visibility',
options: [],
filterKey: 'lastSeenAt',
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
},
{
label: 'Flag type',
icon: 'flag',

View File

@ -38,6 +38,7 @@ export const useProjectFeatureSearch = (
tag: FilterItemParam,
state: FilterItemParam,
createdAt: FilterItemParam,
lastSeenAt: FilterItemParam,
type: FilterItemParam,
createdBy: FilterItemParam,
archived: FilterItemParam,

View File

@ -70,4 +70,8 @@ export type SearchFeaturesParams = {
* The date the feature was created. The date can be specified with an operator. The supported operators are IS_BEFORE, IS_ON_OR_AFTER.
*/
createdAt?: string;
/**
* The date the feature was last seen (either from metrics or manual report). The date can be specified with an operator. The supported operators are IS_BEFORE, IS_ON_OR_AFTER.
*/
lastSeenAt?: string;
};

View File

@ -108,6 +108,7 @@ export default class FeatureSearchController extends Controller {
favoritesFirst,
archived,
sortBy,
lastSeenAt,
} = req.query;
const userId = req.user.id;
const {
@ -149,6 +150,7 @@ export default class FeatureSearchController extends Controller {
createdBy,
sortBy,
lifecycle,
lastSeenAt,
status: normalizedStatus,
offset: normalizedOffset,
limit: normalizedLimit,

View File

@ -73,6 +73,14 @@ export class FeatureSearchService {
if (parsed) queryParams.push(parsed);
}
if (params.lastSeenAt) {
const parsed = parseSearchOperatorValue(
'lastSeenAt',
params.lastSeenAt,
);
if (parsed) queryParams.push(parsed);
}
['tag', 'segment', 'project'].forEach((field) => {
if (params[field]) {
const parsed = parseSearchOperatorValue(field, params[field]);

View File

@ -771,6 +771,26 @@ const applyStaleConditions = (
}
}
};
const applyLastSeenAtConditions = (
query: Knex.QueryBuilder,
lastSeenAtConditions: IQueryParam[],
): void => {
lastSeenAtConditions.forEach((param) => {
const lastSeenAtExpression = query.client.raw(
'coalesce(last_seen_at_metrics.last_seen_at, features.last_seen_at)',
);
switch (param.operator) {
case 'IS_BEFORE':
query.where(lastSeenAtExpression, '<', param.values[0]);
break;
case 'IS_ON_OR_AFTER':
query.where(lastSeenAtExpression, '>=', param.values[0]);
break;
}
});
};
const applyQueryParams = (
query: Knex.QueryBuilder,
queryParams: IQueryParam[],
@ -782,12 +802,17 @@ const applyQueryParams = (
const segmentConditions = queryParams.filter(
(param) => param.field === 'segment',
);
const lastSeenAtConditions = queryParams.filter(
(param) => param.field === 'lastSeenAt',
);
const genericConditions = queryParams.filter(
(param) => !['tag', 'stale'].includes(param.field),
(param) =>
!['tag', 'stale', 'segment', 'lastSeenAt'].includes(param.field),
);
applyGenericQueryParams(query, genericConditions);
applyStaleConditions(query, staleConditions);
applyLastSeenAtConditions(query, lastSeenAtConditions);
applyMultiQueryParams(
query,

View File

@ -1085,6 +1085,56 @@ test('should filter features by combined operators', async () => {
});
});
test('should filter features by lastSeenAt', async () => {
await app.createFeature({
name: 'recently_seen_feature',
});
await app.createFeature({
name: 'old_seen_feature',
});
// Insert lastSeenAt data for both features
const recentDate = new Date();
const oldDate = new Date();
oldDate.setDate(oldDate.getDate() - 10); // 10 days ago
await insertLastSeenAt(
'recently_seen_feature',
db.rawDatabase,
DEFAULT_ENV,
recentDate.toISOString(),
);
await insertLastSeenAt(
'old_seen_feature',
db.rawDatabase,
DEFAULT_ENV,
oldDate.toISOString(),
);
// Filter for features seen in the last 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const { body: recentFeatures } = await app.request
.get(
`/api/admin/search/features?lastSeenAt=IS_ON_OR_AFTER:${sevenDaysAgo.toISOString()}`,
)
.expect(200);
expect(recentFeatures.features).toHaveLength(1);
expect(recentFeatures.features[0].name).toBe('recently_seen_feature');
// Filter for features seen before 7 days ago
const { body: oldFeatures } = await app.request
.get(
`/api/admin/search/features?lastSeenAt=IS_BEFORE:${sevenDaysAgo.toISOString()}`,
)
.expect(200);
expect(oldFeatures.features).toHaveLength(1);
expect(oldFeatures.features[0].name).toBe('old_seen_feature');
});
test('should return environment usage metrics and lifecycle', async () => {
await app.createFeature({
name: 'my_feature_b',

View File

@ -31,6 +31,7 @@ export interface IFeatureSearchParams {
type?: string;
tag?: string;
lifecycle?: string;
lastSeenAt?: string;
status?: string[][];
offset: number;
favoritesFirst?: boolean;

View File

@ -179,6 +179,17 @@ export const featureSearchQueryParameters = [
'The date the feature was created. The date can be specified with an operator. The supported operators are IS_BEFORE, IS_ON_OR_AFTER.',
in: 'query',
},
{
name: 'lastSeenAt',
schema: {
type: 'string',
example: 'IS_ON_OR_AFTER:2023-01-28',
pattern: '^(IS_BEFORE|IS_ON_OR_AFTER):\\d{4}-\\d{2}-\\d{2}$',
},
description:
'The date the feature was last seen from metrics. The date can be specified with an operator. The supported operators are IS_BEFORE, IS_ON_OR_AFTER.',
in: 'query',
},
] as const;
export type FeatureSearchQueryParameters = Partial<