mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
parent
3d40d0d6db
commit
ae19cae8a9
@ -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<HTMLDivElement>(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 (
|
||||
<PageContent
|
||||
disablePadding={displayInline}
|
||||
disableBorder={displayInline}
|
||||
header={
|
||||
<PageHeader
|
||||
title={title}
|
||||
title={`${title} (${countText})`}
|
||||
actions={
|
||||
<>
|
||||
{showDataSwitch}
|
||||
|
@ -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<IEvent[]>();
|
||||
const [totalEvents, setTotalEvents] = useState<number>(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,
|
||||
};
|
||||
};
|
||||
|
@ -57,6 +57,36 @@ class EventStore extends AnyEventEmitter implements IEventStore {
|
||||
}
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
let count = await this.db(TABLE)
|
||||
.count<Record<string, number>>()
|
||||
.first();
|
||||
if (typeof count.count === 'string') {
|
||||
return parseInt(count.count, 10);
|
||||
} else {
|
||||
return count.count;
|
||||
}
|
||||
}
|
||||
|
||||
async filteredCount(eventSearch: SearchEventsSchema): Promise<number> {
|
||||
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<void> {
|
||||
try {
|
||||
const savedRows = await this.db(TABLE)
|
||||
|
@ -16,6 +16,10 @@ export const eventsSchema = {
|
||||
type: 'array',
|
||||
items: { $ref: eventSchema.$id },
|
||||
},
|
||||
totalEvents: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
|
@ -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<EventsSchema>,
|
||||
): Promise<void> {
|
||||
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<FeatureEventsSchema>,
|
||||
): Promise<void> {
|
||||
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<unknown, unknown, SearchEventsSchema>,
|
||||
res: Response<EventsSchema>,
|
||||
): Promise<void> {
|
||||
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(
|
||||
|
@ -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<IEvent[]> {
|
||||
return this.eventStore.getEvents();
|
||||
async getEvents(): Promise<IEventList> {
|
||||
let totalEvents = await this.eventStore.count();
|
||||
let events = await this.eventStore.getEvents();
|
||||
return {
|
||||
events,
|
||||
totalEvents,
|
||||
};
|
||||
}
|
||||
|
||||
async searchEvents(search: SearchEventsSchema): Promise<IEvent[]> {
|
||||
return this.eventStore.searchEvents(search);
|
||||
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
|
||||
let totalEvents = await this.eventStore.filteredCount(search);
|
||||
let events = await this.eventStore.searchEvents(search);
|
||||
return {
|
||||
events,
|
||||
totalEvents,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -7,5 +7,7 @@ export interface IEventStore extends Store<IEvent, number>, EventEmitter {
|
||||
store(event: IBaseEvent): Promise<void>;
|
||||
batchStore(events: IBaseEvent[]): Promise<void>;
|
||||
getEvents(): Promise<IEvent[]>;
|
||||
count(): Promise<number>;
|
||||
filteredCount(search: SearchEventsSchema): Promise<number>;
|
||||
searchEvents(search: SearchEventsSchema): Promise<IEvent[]>;
|
||||
}
|
||||
|
@ -905,6 +905,10 @@ Object {
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"totalEvents": Object {
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
},
|
||||
"version": Object {
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
|
8
src/test/fixtures/fake-event-store.ts
vendored
8
src/test/fixtures/fake-event-store.ts
vendored
@ -40,6 +40,14 @@ class FakeEventStore extends AnyEventEmitter implements IEventStore {
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return Promise.resolve(this.events.length);
|
||||
}
|
||||
|
||||
filteredCount(): Promise<number> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async exists(key: number): Promise<boolean> {
|
||||
|
Loading…
Reference in New Issue
Block a user