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:
parent
2629705501
commit
c9df6c370f
@ -105,6 +105,13 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
|||||||
filterKey: 'createdAt',
|
filterKey: 'createdAt',
|
||||||
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
|
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',
|
label: 'Flag type',
|
||||||
icon: 'flag',
|
icon: 'flag',
|
||||||
|
@ -31,6 +31,7 @@ export const useGlobalFeatureSearch = (pageLimit = DEFAULT_PAGE_LIMIT) => {
|
|||||||
state: FilterItemParam,
|
state: FilterItemParam,
|
||||||
segment: FilterItemParam,
|
segment: FilterItemParam,
|
||||||
createdAt: FilterItemParam,
|
createdAt: FilterItemParam,
|
||||||
|
lastSeenAt: FilterItemParam,
|
||||||
type: FilterItemParam,
|
type: FilterItemParam,
|
||||||
lifecycle: FilterItemParam,
|
lifecycle: FilterItemParam,
|
||||||
createdBy: FilterItemParam,
|
createdBy: FilterItemParam,
|
||||||
|
@ -81,6 +81,13 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
|
|||||||
filterKey: 'createdAt',
|
filterKey: 'createdAt',
|
||||||
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
|
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',
|
label: 'Flag type',
|
||||||
icon: 'flag',
|
icon: 'flag',
|
||||||
|
@ -38,6 +38,7 @@ export const useProjectFeatureSearch = (
|
|||||||
tag: FilterItemParam,
|
tag: FilterItemParam,
|
||||||
state: FilterItemParam,
|
state: FilterItemParam,
|
||||||
createdAt: FilterItemParam,
|
createdAt: FilterItemParam,
|
||||||
|
lastSeenAt: FilterItemParam,
|
||||||
type: FilterItemParam,
|
type: FilterItemParam,
|
||||||
createdBy: FilterItemParam,
|
createdBy: FilterItemParam,
|
||||||
archived: FilterItemParam,
|
archived: FilterItemParam,
|
||||||
|
@ -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.
|
* 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;
|
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;
|
||||||
};
|
};
|
||||||
|
@ -108,6 +108,7 @@ export default class FeatureSearchController extends Controller {
|
|||||||
favoritesFirst,
|
favoritesFirst,
|
||||||
archived,
|
archived,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
lastSeenAt,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const {
|
const {
|
||||||
@ -149,6 +150,7 @@ export default class FeatureSearchController extends Controller {
|
|||||||
createdBy,
|
createdBy,
|
||||||
sortBy,
|
sortBy,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
|
lastSeenAt,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
offset: normalizedOffset,
|
offset: normalizedOffset,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
|
@ -73,6 +73,14 @@ export class FeatureSearchService {
|
|||||||
if (parsed) queryParams.push(parsed);
|
if (parsed) queryParams.push(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.lastSeenAt) {
|
||||||
|
const parsed = parseSearchOperatorValue(
|
||||||
|
'lastSeenAt',
|
||||||
|
params.lastSeenAt,
|
||||||
|
);
|
||||||
|
if (parsed) queryParams.push(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
['tag', 'segment', 'project'].forEach((field) => {
|
['tag', 'segment', 'project'].forEach((field) => {
|
||||||
if (params[field]) {
|
if (params[field]) {
|
||||||
const parsed = parseSearchOperatorValue(field, params[field]);
|
const parsed = parseSearchOperatorValue(field, params[field]);
|
||||||
|
@ -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 = (
|
const applyQueryParams = (
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
queryParams: IQueryParam[],
|
queryParams: IQueryParam[],
|
||||||
@ -782,12 +802,17 @@ const applyQueryParams = (
|
|||||||
const segmentConditions = queryParams.filter(
|
const segmentConditions = queryParams.filter(
|
||||||
(param) => param.field === 'segment',
|
(param) => param.field === 'segment',
|
||||||
);
|
);
|
||||||
|
const lastSeenAtConditions = queryParams.filter(
|
||||||
|
(param) => param.field === 'lastSeenAt',
|
||||||
|
);
|
||||||
const genericConditions = queryParams.filter(
|
const genericConditions = queryParams.filter(
|
||||||
(param) => !['tag', 'stale'].includes(param.field),
|
(param) =>
|
||||||
|
!['tag', 'stale', 'segment', 'lastSeenAt'].includes(param.field),
|
||||||
);
|
);
|
||||||
applyGenericQueryParams(query, genericConditions);
|
applyGenericQueryParams(query, genericConditions);
|
||||||
|
|
||||||
applyStaleConditions(query, staleConditions);
|
applyStaleConditions(query, staleConditions);
|
||||||
|
applyLastSeenAtConditions(query, lastSeenAtConditions);
|
||||||
|
|
||||||
applyMultiQueryParams(
|
applyMultiQueryParams(
|
||||||
query,
|
query,
|
||||||
|
@ -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 () => {
|
test('should return environment usage metrics and lifecycle', async () => {
|
||||||
await app.createFeature({
|
await app.createFeature({
|
||||||
name: 'my_feature_b',
|
name: 'my_feature_b',
|
||||||
|
@ -31,6 +31,7 @@ export interface IFeatureSearchParams {
|
|||||||
type?: string;
|
type?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
lifecycle?: string;
|
lifecycle?: string;
|
||||||
|
lastSeenAt?: string;
|
||||||
status?: string[][];
|
status?: string[][];
|
||||||
offset: number;
|
offset: number;
|
||||||
favoritesFirst?: boolean;
|
favoritesFirst?: boolean;
|
||||||
|
@ -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.',
|
'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',
|
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;
|
] as const;
|
||||||
|
|
||||||
export type FeatureSearchQueryParameters = Partial<
|
export type FeatureSearchQueryParameters = Partial<
|
||||||
|
Loading…
Reference in New Issue
Block a user