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,
|
displayInline,
|
||||||
}: IEventLogProps) => {
|
}: IEventLogProps) => {
|
||||||
const [query, setQuery] = useState('');
|
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 fetchNextPageRef = useOnVisible<HTMLDivElement>(fetchNextPage);
|
||||||
const { eventSettings, setEventSettings } = useEventSettings();
|
const { eventSettings, setEventSettings } = useEventSettings();
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
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 (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
disablePadding={displayInline}
|
disablePadding={displayInline}
|
||||||
disableBorder={displayInline}
|
disableBorder={displayInline}
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={title}
|
title={`${title} (${countText})`}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
{showDataSwitch}
|
{showDataSwitch}
|
||||||
|
@ -10,6 +10,7 @@ export interface IUseEventSearchOutput {
|
|||||||
events?: IEvent[];
|
events?: IEvent[];
|
||||||
fetchNextPage: () => void;
|
fetchNextPage: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
totalEvents?: number;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export const useEventSearch = (
|
|||||||
query?: string
|
query?: string
|
||||||
): IUseEventSearchOutput => {
|
): IUseEventSearchOutput => {
|
||||||
const [events, setEvents] = useState<IEvent[]>();
|
const [events, setEvents] = useState<IEvent[]>();
|
||||||
|
const [totalEvents, setTotalEvents] = useState<number>(0);
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
const search: IEventSearch = useMemo(
|
const search: IEventSearch = useMemo(
|
||||||
@ -35,14 +37,15 @@ export const useEventSearch = (
|
|||||||
[project, feature, query]
|
[project, feature, query]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, error, isValidating } = useSWR<{ events: IEvent[] }>(
|
const { data, error, isValidating } = useSWR<{
|
||||||
[PATH, search, offset],
|
events: IEvent[];
|
||||||
() => searchEvents(PATH, { ...search, offset })
|
totalEvents?: number;
|
||||||
);
|
}>([PATH, search, offset], () => searchEvents(PATH, { ...search, offset }));
|
||||||
|
|
||||||
// Reset the page when there are new search conditions.
|
// Reset the page when there are new search conditions.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
|
setTotalEvents(0);
|
||||||
setEvents(undefined);
|
setEvents(undefined);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
@ -50,6 +53,9 @@ export const useEventSearch = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setEvents(prev => [...(prev ?? []), ...data.events]);
|
setEvents(prev => [...(prev ?? []), ...data.events]);
|
||||||
|
if (data.totalEvents) {
|
||||||
|
setTotalEvents(data.totalEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@ -64,6 +70,7 @@ export const useEventSearch = (
|
|||||||
events,
|
events,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
|
totalEvents,
|
||||||
error,
|
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> {
|
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const savedRows = await this.db(TABLE)
|
const savedRows = await this.db(TABLE)
|
||||||
|
@ -16,6 +16,10 @@ export const eventsSchema = {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { $ref: eventSchema.$id },
|
items: { $ref: eventSchema.$id },
|
||||||
},
|
},
|
||||||
|
totalEvents: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -3,7 +3,7 @@ import { IUnleashConfig } from '../../types/option';
|
|||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import EventService from '../../services/event-service';
|
import EventService from '../../services/event-service';
|
||||||
import { ADMIN, NONE } from '../../types/permissions';
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
import { IEvent } from '../../types/events';
|
import { IEvent, IEventList } from '../../types/events';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { anonymise } from '../../util/anonymise';
|
import { anonymise } from '../../util/anonymise';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
@ -121,16 +121,17 @@ export default class EventController extends Controller {
|
|||||||
res: Response<EventsSchema>,
|
res: Response<EventsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { project } = req.query;
|
const { project } = req.query;
|
||||||
let events: IEvent[];
|
let eventList: IEventList;
|
||||||
if (project) {
|
if (project) {
|
||||||
events = await this.eventService.searchEvents({ project });
|
eventList = await this.eventService.searchEvents({ project });
|
||||||
} else {
|
} else {
|
||||||
events = await this.eventService.getEvents();
|
eventList = await this.eventService.getEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: EventsSchema = {
|
const response: EventsSchema = {
|
||||||
version,
|
version,
|
||||||
events: serializeDates(this.maybeAnonymiseEvents(events)),
|
events: serializeDates(this.maybeAnonymiseEvents(eventList.events)),
|
||||||
|
totalEvents: eventList.totalEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
@ -146,12 +147,13 @@ export default class EventController extends Controller {
|
|||||||
res: Response<FeatureEventsSchema>,
|
res: Response<FeatureEventsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const feature = req.params.featureName;
|
const feature = req.params.featureName;
|
||||||
const events = await this.eventService.searchEvents({ feature });
|
const eventList = await this.eventService.searchEvents({ feature });
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
version,
|
version,
|
||||||
toggleName: feature,
|
toggleName: feature,
|
||||||
events: serializeDates(this.maybeAnonymiseEvents(events)),
|
events: serializeDates(this.maybeAnonymiseEvents(eventList.events)),
|
||||||
|
totalEvents: eventList.totalEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
@ -166,11 +168,12 @@ export default class EventController extends Controller {
|
|||||||
req: Request<unknown, unknown, SearchEventsSchema>,
|
req: Request<unknown, unknown, SearchEventsSchema>,
|
||||||
res: Response<EventsSchema>,
|
res: Response<EventsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const events = await this.eventService.searchEvents(req.body);
|
const eventList = await this.eventService.searchEvents(req.body);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
version,
|
version,
|
||||||
events: serializeDates(this.maybeAnonymiseEvents(events)),
|
events: serializeDates(this.maybeAnonymiseEvents(eventList.events)),
|
||||||
|
totalEvents: eventList.totalEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
|
@ -2,7 +2,7 @@ import { IUnleashConfig } from '../types/option';
|
|||||||
import { IUnleashStores } from '../types/stores';
|
import { IUnleashStores } from '../types/stores';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IEventStore } from '../types/stores/event-store';
|
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';
|
import { SearchEventsSchema } from '../openapi/spec/search-events-schema';
|
||||||
|
|
||||||
export default class EventService {
|
export default class EventService {
|
||||||
@ -18,12 +18,22 @@ export default class EventService {
|
|||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvents(): Promise<IEvent[]> {
|
async getEvents(): Promise<IEventList> {
|
||||||
return this.eventStore.getEvents();
|
let totalEvents = await this.eventStore.count();
|
||||||
|
let events = await this.eventStore.getEvents();
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
totalEvents,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchEvents(search: SearchEventsSchema): Promise<IEvent[]> {
|
async searchEvents(search: SearchEventsSchema): Promise<IEventList> {
|
||||||
return this.eventStore.searchEvents(search);
|
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;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEventList {
|
||||||
|
totalEvents: number;
|
||||||
|
events: IEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
class BaseEvent implements IBaseEvent {
|
class BaseEvent implements IBaseEvent {
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
|
|
||||||
|
@ -7,5 +7,7 @@ export interface IEventStore extends Store<IEvent, number>, EventEmitter {
|
|||||||
store(event: IBaseEvent): Promise<void>;
|
store(event: IBaseEvent): Promise<void>;
|
||||||
batchStore(events: IBaseEvent[]): Promise<void>;
|
batchStore(events: IBaseEvent[]): Promise<void>;
|
||||||
getEvents(): Promise<IEvent[]>;
|
getEvents(): Promise<IEvent[]>;
|
||||||
|
count(): Promise<number>;
|
||||||
|
filteredCount(search: SearchEventsSchema): Promise<number>;
|
||||||
searchEvents(search: SearchEventsSchema): Promise<IEvent[]>;
|
searchEvents(search: SearchEventsSchema): Promise<IEvent[]>;
|
||||||
}
|
}
|
||||||
|
@ -905,6 +905,10 @@ Object {
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"totalEvents": Object {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
"version": Object {
|
"version": Object {
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "integer",
|
"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 = [];
|
this.events = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async count(): Promise<number> {
|
||||||
|
return Promise.resolve(this.events.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredCount(): Promise<number> {
|
||||||
|
throw new Error('Method not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {}
|
destroy(): void {}
|
||||||
|
|
||||||
async exists(key: number): Promise<boolean> {
|
async exists(key: number): Promise<boolean> {
|
||||||
|
Loading…
Reference in New Issue
Block a user