From ae19cae8a990eae270a43337d21ba7e25571cd68 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Fri, 2 Sep 2022 08:35:31 +0200 Subject: [PATCH] feat: add count to event list (#2036) * feat: add count to event list --- .../component/events/EventLog/EventLog.tsx | 12 ++++++-- .../getters/useEventSearch/useEventSearch.ts | 15 +++++++--- src/lib/db/event-store.ts | 30 +++++++++++++++++++ src/lib/openapi/spec/events-schema.ts | 4 +++ src/lib/routes/admin-api/event.ts | 21 +++++++------ src/lib/services/event-service.ts | 20 +++++++++---- src/lib/types/events.ts | 5 ++++ src/lib/types/stores/event-store.ts | 2 ++ .../__snapshots__/openapi.e2e.test.ts.snap | 4 +++ src/test/fixtures/fake-event-store.ts | 8 +++++ 10 files changed, 101 insertions(+), 20 deletions(-) diff --git a/frontend/src/component/events/EventLog/EventLog.tsx b/frontend/src/component/events/EventLog/EventLog.tsx index 288f9ade28..21f06a7378 100644 --- a/frontend/src/component/events/EventLog/EventLog.tsx +++ b/frontend/src/component/events/EventLog/EventLog.tsx @@ -35,7 +35,11 @@ export const EventLog = ({ displayInline, }: IEventLogProps) => { const [query, setQuery] = useState(''); - const { events, fetchNextPage } = useEventSearch(project, feature, query); + const { events, totalEvents, fetchNextPage } = useEventSearch( + project, + feature, + query + ); const fetchNextPageRef = useOnVisible(fetchNextPage); const { eventSettings, setEventSettings } = useEventSettings(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); @@ -64,13 +68,17 @@ export const EventLog = ({ /> ); + let count = events?.length || 0; + let totalCount = totalEvents || 0; + let countText = `${count} of ${totalCount}`; + return ( {showDataSwitch} diff --git a/frontend/src/hooks/api/getters/useEventSearch/useEventSearch.ts b/frontend/src/hooks/api/getters/useEventSearch/useEventSearch.ts index c5d1572990..2ada2e7244 100644 --- a/frontend/src/hooks/api/getters/useEventSearch/useEventSearch.ts +++ b/frontend/src/hooks/api/getters/useEventSearch/useEventSearch.ts @@ -10,6 +10,7 @@ export interface IUseEventSearchOutput { events?: IEvent[]; fetchNextPage: () => void; loading: boolean; + totalEvents?: number; error?: Error; } @@ -28,6 +29,7 @@ export const useEventSearch = ( query?: string ): IUseEventSearchOutput => { const [events, setEvents] = useState(); + const [totalEvents, setTotalEvents] = useState(0); const [offset, setOffset] = useState(0); const search: IEventSearch = useMemo( @@ -35,14 +37,15 @@ export const useEventSearch = ( [project, feature, query] ); - const { data, error, isValidating } = useSWR<{ events: IEvent[] }>( - [PATH, search, offset], - () => searchEvents(PATH, { ...search, offset }) - ); + const { data, error, isValidating } = useSWR<{ + events: IEvent[]; + totalEvents?: number; + }>([PATH, search, offset], () => searchEvents(PATH, { ...search, offset })); // Reset the page when there are new search conditions. useEffect(() => { setOffset(0); + setTotalEvents(0); setEvents(undefined); }, [search]); @@ -50,6 +53,9 @@ export const useEventSearch = ( useEffect(() => { if (data) { setEvents(prev => [...(prev ?? []), ...data.events]); + if (data.totalEvents) { + setTotalEvents(data.totalEvents); + } } }, [data]); @@ -64,6 +70,7 @@ export const useEventSearch = ( events, loading: !error && !data, fetchNextPage, + totalEvents, error, }; }; diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index e0790473b1..33e0650cf3 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -57,6 +57,36 @@ class EventStore extends AnyEventEmitter implements IEventStore { } } + async count(): Promise { + let count = await this.db(TABLE) + .count>() + .first(); + if (typeof count.count === 'string') { + return parseInt(count.count, 10); + } else { + return count.count; + } + } + + async filteredCount(eventSearch: SearchEventsSchema): Promise { + let query = this.db(TABLE); + if (eventSearch.type) { + query = query.andWhere({ type: eventSearch.type }); + } + if (eventSearch.project) { + query = query.andWhere({ project: eventSearch.project }); + } + if (eventSearch.feature) { + query = query.andWhere({ feature_name: eventSearch.feature }); + } + let count = await query.count().first(); + if (typeof count.count === 'string') { + return parseInt(count.count, 10); + } else { + return count.count; + } + } + async batchStore(events: IBaseEvent[]): Promise { try { const savedRows = await this.db(TABLE) diff --git a/src/lib/openapi/spec/events-schema.ts b/src/lib/openapi/spec/events-schema.ts index efa7137253..912d8981e5 100644 --- a/src/lib/openapi/spec/events-schema.ts +++ b/src/lib/openapi/spec/events-schema.ts @@ -16,6 +16,10 @@ export const eventsSchema = { type: 'array', items: { $ref: eventSchema.$id }, }, + totalEvents: { + type: 'integer', + minimum: 0, + }, }, components: { schemas: { diff --git a/src/lib/routes/admin-api/event.ts b/src/lib/routes/admin-api/event.ts index 3f0fd8dec7..76d5413f15 100644 --- a/src/lib/routes/admin-api/event.ts +++ b/src/lib/routes/admin-api/event.ts @@ -3,7 +3,7 @@ import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; import EventService from '../../services/event-service'; import { ADMIN, NONE } from '../../types/permissions'; -import { IEvent } from '../../types/events'; +import { IEvent, IEventList } from '../../types/events'; import Controller from '../controller'; import { anonymise } from '../../util/anonymise'; import { OpenApiService } from '../../services/openapi-service'; @@ -121,16 +121,17 @@ export default class EventController extends Controller { res: Response, ): Promise { const { project } = req.query; - let events: IEvent[]; + let eventList: IEventList; if (project) { - events = await this.eventService.searchEvents({ project }); + eventList = await this.eventService.searchEvents({ project }); } else { - events = await this.eventService.getEvents(); + eventList = await this.eventService.getEvents(); } const response: EventsSchema = { version, - events: serializeDates(this.maybeAnonymiseEvents(events)), + events: serializeDates(this.maybeAnonymiseEvents(eventList.events)), + totalEvents: eventList.totalEvents, }; this.openApiService.respondWithValidation( @@ -146,12 +147,13 @@ export default class EventController extends Controller { res: Response, ): Promise { const feature = req.params.featureName; - const events = await this.eventService.searchEvents({ feature }); + const eventList = await this.eventService.searchEvents({ feature }); const response = { version, toggleName: feature, - events: serializeDates(this.maybeAnonymiseEvents(events)), + events: serializeDates(this.maybeAnonymiseEvents(eventList.events)), + totalEvents: eventList.totalEvents, }; this.openApiService.respondWithValidation( @@ -166,11 +168,12 @@ export default class EventController extends Controller { req: Request, res: Response, ): Promise { - const events = await this.eventService.searchEvents(req.body); + const eventList = await this.eventService.searchEvents(req.body); const response = { version, - events: serializeDates(this.maybeAnonymiseEvents(events)), + events: serializeDates(this.maybeAnonymiseEvents(eventList.events)), + totalEvents: eventList.totalEvents, }; this.openApiService.respondWithValidation( diff --git a/src/lib/services/event-service.ts b/src/lib/services/event-service.ts index 0f878d3557..dd81c52968 100644 --- a/src/lib/services/event-service.ts +++ b/src/lib/services/event-service.ts @@ -2,7 +2,7 @@ import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { Logger } from '../logger'; import { IEventStore } from '../types/stores/event-store'; -import { IEvent } from '../types/events'; +import { IEventList } from '../types/events'; import { SearchEventsSchema } from '../openapi/spec/search-events-schema'; export default class EventService { @@ -18,12 +18,22 @@ export default class EventService { this.eventStore = eventStore; } - async getEvents(): Promise { - return this.eventStore.getEvents(); + async getEvents(): Promise { + let totalEvents = await this.eventStore.count(); + let events = await this.eventStore.getEvents(); + return { + events, + totalEvents, + }; } - async searchEvents(search: SearchEventsSchema): Promise { - return this.eventStore.searchEvents(search); + async searchEvents(search: SearchEventsSchema): Promise { + let totalEvents = await this.eventStore.filteredCount(search); + let events = await this.eventStore.searchEvents(search); + return { + events, + totalEvents, + }; } } diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index fd8891ce8b..b01b505b1e 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -92,6 +92,11 @@ export interface IEvent extends IBaseEvent { createdAt: Date; } +export interface IEventList { + totalEvents: number; + events: IEvent[]; +} + class BaseEvent implements IBaseEvent { readonly type: string; diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index 2f553b63df..516783b7e0 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -7,5 +7,7 @@ export interface IEventStore extends Store, EventEmitter { store(event: IBaseEvent): Promise; batchStore(events: IBaseEvent[]): Promise; getEvents(): Promise; + count(): Promise; + filteredCount(search: SearchEventsSchema): Promise; searchEvents(search: SearchEventsSchema): Promise; } diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 078bbcd273..58cc7af104 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -905,6 +905,10 @@ Object { }, "type": "array", }, + "totalEvents": Object { + "minimum": 0, + "type": "integer", + }, "version": Object { "minimum": 1, "type": "integer", diff --git a/src/test/fixtures/fake-event-store.ts b/src/test/fixtures/fake-event-store.ts index dbd2505a00..1feb298dc4 100644 --- a/src/test/fixtures/fake-event-store.ts +++ b/src/test/fixtures/fake-event-store.ts @@ -40,6 +40,14 @@ class FakeEventStore extends AnyEventEmitter implements IEventStore { this.events = []; } + async count(): Promise { + return Promise.resolve(this.events.length); + } + + filteredCount(): Promise { + throw new Error('Method not implemented'); + } + destroy(): void {} async exists(key: number): Promise {