Skip to content

Commit

Permalink
fix: add filter strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
totraev authored and jeremy-babylonlabs committed Jan 15, 2025
1 parent 65d7186 commit 5f4078d
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ const options = [
];

export const FinalityProviderFilter = () => {
const { filterValue, handleFilter, searchValue } = useFinalityProviderState();
const { filter, handleFilter } = useFinalityProviderState();

return (
<Select
options={options}
onSelect={handleFilter}
onSelect={(value) => handleFilter("status", value.toString())}
placeholder="Select Status"
value={searchValue ? "" : filterValue}
disabled={Boolean(searchValue)}
value={filter.search ? "" : filter.status}
disabled={Boolean(filter.search)}
renderSelectedOption={(option) => `Showing ${option.label}`}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import cancelCircle from "@/app/assets/cancel-circle.svg";
import { useFinalityProviderState } from "@/app/state/FinalityProviderState";

export const FinalityProviderSearch = () => {
const { handleSearch, searchValue } = useFinalityProviderState();
const { filter, handleFilter } = useFinalityProviderState();

const onSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
handleSearch(e.target.value);
handleFilter("search", e.target.value);
},
[handleSearch],
[handleFilter],
);

const onClearSearch = useCallback(() => {
handleSearch("");
}, [handleSearch]);
handleFilter("search", "");
}, [handleFilter]);

const searchSuffix = searchValue ? (
const searchSuffix = filter.search ? (
<button
onClick={onClearSearch}
className="flex items-center hover:opacity-80 transition-opacity"
Expand All @@ -43,7 +43,7 @@ export const FinalityProviderSearch = () => {
<Input
placeholder="Search by Name or Public Key"
suffix={searchSuffix}
value={searchValue}
value={filter.search}
onChange={onSearchChange}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ export const FinalityProviderTable = ({
isFetching,
finalityProviders,
hasNextPage,
fetchNextPage,
searchValue,
filterValue,
hasError,
filter,
fetchNextPage,
isRowSelectable,
} = useFinalityProviderState();

Expand Down Expand Up @@ -83,7 +82,7 @@ export const FinalityProviderTable = ({
return (
<div className="h-[21rem] overflow-y-auto ">
<Table
key={`${searchValue}-${filterValue}`}
key={`${filter.search}-${filter.status}`}
data={tableData}
columns={finalityProviderColumns}
loading={isFetching}
Expand Down
146 changes: 48 additions & 98 deletions src/app/state/FinalityProviderState.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { useDebounce } from "@uidotdev/usehooks";
import { useSearchParams } from "next/navigation";
import {
Suspense,
useCallback,
useMemo,
useState,
type PropsWithChildren,
} from "react";
import { useCallback, useMemo, useState, type PropsWithChildren } from "react";

import { useFinalityProviders } from "@/app/hooks/client/api/useFinalityProviders";
import {
Expand All @@ -20,42 +14,56 @@ interface SortState {
direction?: "asc" | "desc";
}

const SORT_DIRECTIONS = {
undefined: "desc",
desc: "asc",
asc: undefined,
} as const;

const inactiveStatuses = new Set([
FinalityProviderStateEnum.INACTIVE,
FinalityProviderStateEnum.JAILED,
FinalityProviderStateEnum.SLASHED,
]);
interface FilterState {
search: string;
status: "active" | "inactive" | "";
}

interface FinalityProviderState {
searchValue: string;
filterValue: string | number;
filter: FilterState;
finalityProviders: FinalityProvider[];
hasNextPage: boolean;
isFetching: boolean;
hasError: boolean;
handleSearch: (searchTerm: string) => void;
handleSort: (sortField: string) => void;
handleFilter: (value: string | number) => void;
handleFilter: (key: keyof FilterState, value: string) => void;
isRowSelectable: (row: FinalityProvider) => boolean;
getFinalityProvider: (btcPkHex: string) => FinalityProvider | null;
fetchNextPage: () => void;
}

const SORT_DIRECTIONS = {
undefined: "desc",
desc: "asc",
asc: undefined,
} as const;

const STATUS_FILTERS = {
active: (fp: FinalityProvider) =>
fp.state === FinalityProviderStateEnum.ACTIVE,
inactive: (fp: FinalityProvider) =>
fp.state !== FinalityProviderStateEnum.ACTIVE,
};

const FILTERS = {
search: (fp: FinalityProvider, filter: FilterState) => {
const pattern = new RegExp(filter.search, "i");

return (
pattern.test(fp.description?.moniker ?? "") || pattern.test(fp.btcPk)
);
},
status: (fp: FinalityProvider, filter: FilterState) =>
filter.status && !filter.search ? STATUS_FILTERS[filter.status](fp) : true,
};

const defaultState: FinalityProviderState = {
searchValue: "",
filterValue: "",
filter: { search: "", status: "active" },
finalityProviders: [],
hasNextPage: false,
isFetching: false,
hasError: false,
isRowSelectable: () => false,
handleSearch: () => {},
handleSort: () => {},
handleFilter: () => {},
getFinalityProvider: () => null,
Expand All @@ -65,21 +73,16 @@ const defaultState: FinalityProviderState = {
const { StateProvider, useState: useFpState } =
createStateUtils<FinalityProviderState>(defaultState);

function FinalityProviderStateInner({ children }: PropsWithChildren) {
export function FinalityProviderState({ children }: PropsWithChildren) {
const searchParams = useSearchParams();
const fpParam = searchParams.get("fp");

const [searchValue, setSearchValue] = useState(fpParam || "");
const [filterValue, setFilterValue] = useState<string | number>(
fpParam ? "" : "active",
);
const [filter, setFilter] = useState<FilterState>({
search: fpParam || "",
status: "active",
});
const [sortState, setSortState] = useState<SortState>({});
const [previousFilterValue, setPreviousFilterValue] = useState<
string | number
>("active");
const [isInitialLoad, setIsInitialLoad] = useState(true);

const debouncedSearch = useDebounce(searchValue, 300);
const debouncedSearch = useDebounce(filter.search, 300);

const { data, hasNextPage, fetchNextPage, isFetching, isError } =
useFinalityProviders({
Expand All @@ -88,26 +91,9 @@ function FinalityProviderStateInner({ children }: PropsWithChildren) {
name: debouncedSearch,
});

const handleSearch = useCallback(
(searchTerm: string) => {
if (!searchValue && searchTerm) {
setPreviousFilterValue(filterValue);
}

setSearchValue(searchTerm);

if (!(isInitialLoad && fpParam && searchTerm === fpParam)) {
if (searchTerm) {
setFilterValue("");
} else {
setFilterValue(previousFilterValue);
}
} else {
setIsInitialLoad(false);
}
},
[searchValue, filterValue, previousFilterValue, isInitialLoad, fpParam],
);
const handleFilter = useCallback((key: keyof FilterState, value: string) => {
setFilter((state) => ({ ...state, [key]: value }));
}, []);

const handleSort = useCallback((sortField: string) => {
setSortState(({ field, direction }) =>
Expand All @@ -123,11 +109,6 @@ function FinalityProviderStateInner({ children }: PropsWithChildren) {
);
}, []);

const handleFilter = useCallback((value: string | number) => {
setFilterValue(value);
setPreviousFilterValue(value);
}, []);

const isRowSelectable = useCallback((row: FinalityProvider) => {
return (
row.state === FinalityProviderStateEnum.ACTIVE ||
Expand All @@ -138,27 +119,10 @@ function FinalityProviderStateInner({ children }: PropsWithChildren) {
const filteredFinalityProviders = useMemo(() => {
if (!data?.finalityProviders) return [];

return data.finalityProviders.filter((fp: FinalityProvider) => {
if (!fp) return false;

if (searchValue) {
const searchLower = searchValue.toLowerCase();
return (
(fp.description?.moniker?.toLowerCase() || "").includes(
searchLower,
) || (fp.btcPk?.toLowerCase() || "").includes(searchLower)
);
}

const isActive = fp.state === FinalityProviderStateEnum.ACTIVE;
const isInactive = inactiveStatuses.has(fp.state);

if (filterValue === "active") return isActive;
if (filterValue === "inactive") return isInactive;
// default to active
return true;
});
}, [data?.finalityProviders, filterValue, searchValue]);
return data.finalityProviders.filter((fp: FinalityProvider) =>
Object.values(FILTERS).every((filterFn) => filterFn(fp, filter)),
);
}, [data?.finalityProviders, filter]);

const getFinalityProvider = useCallback(
(btcPkHex: string) =>
Expand All @@ -168,27 +132,23 @@ function FinalityProviderStateInner({ children }: PropsWithChildren) {

const state = useMemo(
() => ({
searchValue,
filterValue,
filter,
finalityProviders: filteredFinalityProviders,
isFetching,
hasNextPage,
hasError: isError,
handleSearch,
handleSort,
handleFilter,
isRowSelectable,
getFinalityProvider,
fetchNextPage,
}),
[
searchValue,
filterValue,
filter,
filteredFinalityProviders,
isFetching,
hasNextPage,
isError,
handleSearch,
handleSort,
handleFilter,
isRowSelectable,
Expand All @@ -200,14 +160,4 @@ function FinalityProviderStateInner({ children }: PropsWithChildren) {
return <StateProvider value={state}>{children}</StateProvider>;
}

export function FinalityProviderState({ children }: PropsWithChildren) {
return (
<Suspense
fallback={<StateProvider value={defaultState}>{children}</StateProvider>}
>
<FinalityProviderStateInner>{children}</FinalityProviderStateInner>
</Suspense>
);
}

export { useFpState as useFinalityProviderState };

0 comments on commit 5f4078d

Please sign in to comment.