1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-18 01:18:23 +02:00

feat: new useEventSearch hook (#7757)

Creates a new useEventSearch hook based on the useFeatureSearch hook.
Moves the old useEventSearch hook into useLegacyEventSearch and updates
references to it.

I don't know yet whether this'll work entirely as expected, but I plan
on making any necessary configurations when I implement the state
management in a follow-up PR.

But because this is pretty much a straight copy-paste from
useFeatureSearch (only adjusting types, I think), I also think it might
be possible to turn this into a generic search template. Not sure if now
is the time, but worth thinking about, I think.
This commit is contained in:
Thomas Heartman 2024-08-05 15:12:45 +02:00 committed by GitHub
parent dd71fe32bb
commit cd7697db62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 200 additions and 73 deletions

View File

@ -7,7 +7,7 @@ import { useEventSettings } from 'hooks/useEventSettings';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch'; import { useLegacyEventSearch } from 'hooks/api/getters/useEventSearch/useLegacyEventSearch';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useOnVisible } from 'hooks/useOnVisible'; import { useOnVisible } from 'hooks/useOnVisible';
import { styled } from '@mui/system'; import { styled } from '@mui/system';
@ -43,7 +43,7 @@ const EventResultWrapper = styled('div')(({ theme }) => ({
export const EventLog = ({ title, project, feature }: IEventLogProps) => { export const EventLog = ({ title, project, feature }: IEventLogProps) => {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const { events, totalEvents, fetchNextPage } = useEventSearch( const { events, totalEvents, fetchNextPage } = useLegacyEventSearch(
project, project,
feature, feature,
query, query,

View File

@ -1,89 +1,124 @@
import useSWR from 'swr'; import useSWR, { type SWRConfiguration } from 'swr';
import { useCallback, useState, useEffect, useMemo } from 'react'; import { useCallback, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import type { EventSchema } from 'openapi'; import type { EventSearchResponseSchema, SearchEventsParams } from 'openapi';
import { useClearSWRCache } from 'hooks/useClearSWRCache';
const PATH = formatApiPath('api/admin/events/search'); type UseEventSearchOutput = {
export interface IUseEventSearchOutput {
events?: EventSchema[];
fetchNextPage: () => void;
loading: boolean; loading: boolean;
totalEvents?: number; initialLoad: boolean;
error?: Error; error: string;
} refetch: () => void;
} & EventSearchResponseSchema;
interface IEventSearch { type CacheValue = {
type?: string; total: number;
project?: string; initialLoad: boolean;
feature?: string; [key: string]: number | boolean;
query?: string; };
limit?: number;
offset?: number;
}
export const useEventSearch = ( type InternalCache = Record<string, CacheValue>;
project?: string,
feature?: string,
query?: string,
): IUseEventSearchOutput => {
const [events, setEvents] = useState<EventSchema[]>();
const [totalEvents, setTotalEvents] = useState<number>(0);
const [offset, setOffset] = useState(0);
const search: IEventSearch = useMemo( const fallbackData: EventSearchResponseSchema = {
() => ({ project, feature, query, limit: 50 }), events: [],
[project, feature, query], total: 0,
); };
const { data, error, isValidating } = useSWR<{ const SWR_CACHE_SIZE = 10;
events: EventSchema[]; const PATH = 'api/admin/search/events?';
totalEvents?: number;
}>([PATH, search, offset], () => searchEvents(PATH, { ...search, offset })); const createEventSearch = () => {
const internalCache: InternalCache = {};
const initCache = (id: string) => {
internalCache[id] = {
total: 0,
initialLoad: true,
};
};
const set = (id: string, key: string, value: number | boolean) => {
if (!internalCache[id]) {
initCache(id);
}
internalCache[id][key] = value;
};
const get = (id: string) => {
if (!internalCache[id]) {
initCache(id);
}
return internalCache[id];
};
return (
params: SearchEventsParams,
options: SWRConfiguration = {},
cachePrefix: string = '',
): UseEventSearchOutput => {
const { KEY, fetcher } = getEventSearchFetcher(params);
const swrKey = `${cachePrefix}${KEY}`;
const cacheId = params.project || '';
useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE);
// Reset the page when there are new search conditions.
useEffect(() => { useEffect(() => {
setOffset(0); initCache(params.project || '');
setTotalEvents(0); }, []);
setEvents(undefined);
}, [search]);
// Append results to the page when more data has been fetched. const { data, error, mutate, isLoading } =
useEffect(() => { useSWR<EventSearchResponseSchema>(swrKey, fetcher, options);
if (data) {
setEvents((prev) => [
...(prev?.slice(0, offset) || []),
...data.events,
]);
if (data.totalEvents) {
setTotalEvents(data.totalEvents);
}
}
}, [data]);
// Update the offset to fetch more results at the end of the page. const refetch = useCallback(() => {
const fetchNextPage = useCallback(() => { mutate();
if (events && !isValidating) { }, [mutate]);
setOffset(events.length);
}
}, [events, isValidating]);
const cacheValues = get(cacheId);
if (data?.total !== undefined) {
set(cacheId, 'total', data.total);
}
if (!isLoading && cacheValues.initialLoad) {
set(cacheId, 'initialLoad', false);
}
const returnData = data || fallbackData;
return { return {
events, ...returnData,
loading: !error && !data, loading: isLoading,
fetchNextPage,
totalEvents,
error, error,
refetch,
total: cacheValues.total,
initialLoad: isLoading && cacheValues.initialLoad,
};
}; };
}; };
const searchEvents = (path: string, search: IEventSearch) => { export const DEFAULT_PAGE_LIMIT = 25;
const getEventSearchFetcher = (params: SearchEventsParams) => {
const urlSearchParams = new URLSearchParams(
Array.from(
Object.entries(params)
.filter(([_, value]) => !!value)
.map(([key, value]) => [key, value.toString()]), // TODO: parsing non-string parameters
),
).toString();
const KEY = `${PATH}${urlSearchParams}`;
const fetcher = () => {
const path = formatApiPath(KEY);
return fetch(path, { return fetch(path, {
method: 'POST', method: 'GET',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(search),
}) })
.then(handleErrorResponses('Event history')) .then(handleErrorResponses('Event search'))
.then((res) => res.json()); .then((res) => res.json());
};
return {
fetcher,
KEY,
};
}; };
export const useEventSearch = createEventSearch();

View File

@ -0,0 +1,92 @@
import useSWR from 'swr';
import { useCallback, useState, useEffect, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import type { EventSchema } from 'openapi';
const PATH = formatApiPath('api/admin/events/search');
export interface IUseEventSearchOutput {
events?: EventSchema[];
fetchNextPage: () => void;
loading: boolean;
totalEvents?: number;
error?: Error;
}
interface IEventSearch {
type?: string;
project?: string;
feature?: string;
query?: string;
limit?: number;
offset?: number;
}
/**
* @deprecated Use useEventSearch instead. Remove with flag: newEventSearch
*/
export const useLegacyEventSearch = (
project?: string,
feature?: string,
query?: string,
): IUseEventSearchOutput => {
const [events, setEvents] = useState<EventSchema[]>();
const [totalEvents, setTotalEvents] = useState<number>(0);
const [offset, setOffset] = useState(0);
const search: IEventSearch = useMemo(
() => ({ project, feature, query, limit: 50 }),
[project, feature, query],
);
const { data, error, isValidating } = useSWR<{
events: EventSchema[];
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]);
// Append results to the page when more data has been fetched.
useEffect(() => {
if (data) {
setEvents((prev) => [
...(prev?.slice(0, offset) || []),
...data.events,
]);
if (data.totalEvents) {
setTotalEvents(data.totalEvents);
}
}
}, [data]);
// Update the offset to fetch more results at the end of the page.
const fetchNextPage = useCallback(() => {
if (events && !isValidating) {
setOffset(events.length);
}
}, [events, isValidating]);
return {
events,
loading: !error && !data,
fetchNextPage,
totalEvents,
error,
};
};
const searchEvents = (path: string, search: IEventSearch) => {
return fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(search),
})
.then(handleErrorResponses('Event history'))
.then((res) => res.json());
};