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:
parent
dd71fe32bb
commit
cd7697db62
@ -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,
|
||||||
|
@ -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();
|
||||||
|
@ -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());
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user