Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APR] PoolTable Filter #251

Merged
merged 46 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
39fc62c
Altered calculatePoolStats to return tokens used on Pool
Rawallon Aug 29, 2023
92bf20f
Add PoolType return to calcutePoolStats/Route
Rawallon Aug 29, 2023
f746676
Adjust typings
Rawallon Aug 29, 2023
76a7d7d
Altered tooltip position
Rawallon Aug 29, 2023
dbdd292
Align text to right on PoolTable
Rawallon Aug 29, 2023
04135c9
Moved enum export outside of route.ts
Rawallon Aug 29, 2023
300f999
Added Downshift as a dependency
Rawallon Aug 29, 2023
7b68932
Implemeneted Multi selector with Downshift
Rawallon Aug 29, 2023
74d74dd
WIP: Initial token filter input
Rawallon Aug 29, 2023
13ffa6f
Refactor MultiSelector to get coins from Pools
Rawallon Aug 30, 2023
bd35679
Implemented util function to get filter from URL search params
Rawallon Aug 30, 2023
ab429ae
Revert "fix pool tokens source in APR"
Rawallon Aug 30, 2023
fa65ead
Update pnpm-lock.yaml
Rawallon Aug 30, 2023
e097de9
Refactor getFilteredApiUrl
Rawallon Aug 30, 2023
7c2ea9c
Changed token filter name and added Type
Rawallon Aug 30, 2023
194b68b
Replaced _ by ,
Rawallon Aug 30, 2023
242bda1
Redirect with router
Rawallon Aug 30, 2023
c8d5b91
Format
Rawallon Aug 30, 2023
5e5f411
Fix Typo
Rawallon Aug 31, 2023
74e5953
Styling on MultiSelect
Rawallon Aug 31, 2023
92a6de6
Better Ux on MultiSelect
Rawallon Aug 31, 2023
cb289b6
Fix push on adding token
Rawallon Aug 31, 2023
806d9b9
Refactor PoolTable to use queryParams
Rawallon Aug 31, 2023
ce936a4
Fix sort and order filtering params
Rawallon Aug 31, 2023
99fa134
Removed unused import
Rawallon Aug 31, 2023
bf803ae
Refactor function to recieve roundId
Rawallon Aug 31, 2023
ae6635c
Fix typo
Rawallon Aug 31, 2023
49bcf6e
Adds initial min tvl and limit on filter url
Rawallon Aug 31, 2023
102cf27
Refactor PoolTable sort to use links
Rawallon Aug 31, 2023
417c6e0
Update page.tsx
Rawallon Aug 31, 2023
9e9df4b
Fix bad arg name
Rawallon Aug 31, 2023
4e86d1f
Moved changeHandler to useCallback
Rawallon Aug 31, 2023
ac7c60c
Moved removeSelectedItemByIndex to useCallback
Rawallon Aug 31, 2023
0d8d87f
Import
Rawallon Aug 31, 2023
a6db0b7
Format
Rawallon Aug 31, 2023
0a9b69e
Implemented function to filter selected items
Rawallon Aug 31, 2023
41fab0a
Implemented existing components
Rawallon Aug 31, 2023
d29dfa3
Update MultiSelectDropdown.tsx
Rawallon Aug 31, 2023
50e60a0
Moved MultiSelectDropdown to components folder
Rawallon Aug 31, 2023
013bc7d
Update TokenFilterInput.tsx
Rawallon Aug 31, 2023
f88a9a1
Removed unused args on funcs
Rawallon Aug 31, 2023
e2a924f
Smaller text on the popover
Rawallon Aug 31, 2023
60186d2
Add max width to MultiSelect
Rawallon Aug 31, 2023
ea8984a
Add width value to table header
Rawallon Aug 31, 2023
f8f49b8
Add empty text to poolTable
Rawallon Aug 31, 2023
81cb3f7
Added hasMorePool conditional to PoolTable
Rawallon Aug 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/balancer-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.3",
"downshift": "^8.1.0",
"graphql-request": "5.1.0",
"inputmask": "^5.0.8",
"lodash": "^4.17.21",
Expand Down
177 changes: 177 additions & 0 deletions apps/balancer-tools/src/app/apr/(components)/MultiSelectDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"use client";

import { Cross1Icon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
import Downshift, { ControllerStateAndHelpers } from "downshift";
import React, { useCallback, useState } from "react";

import { Badge } from "#/components/Badge";
import { BaseInput } from "#/components/Input";

export const MultiSelectDropdown = ({
items,
placeholderText,
onSelectionItemsChange,
initialSelectedItems,
...rest
}: {
items: string[];
placeholderText: string;
onSelectionItemsChange: (items: string[]) => void;
initialSelectedItems: string[];
}) => {
const [selectedItems, setSelectedItems] =
useState<string[]>(initialSelectedItems);

function handleOnKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
const inputElement = event.target as HTMLInputElement;
if (!inputElement.value.length) {
if (event.key === "Backspace") {
const copySelectedItems = [...selectedItems];
copySelectedItems.pop();
setSelectedItems(copySelectedItems);
onSelectionItemsChange(copySelectedItems);
}
}
}
Rawallon marked this conversation as resolved.
Show resolved Hide resolved

const changeHandler = useCallback(
(
selectedItems: string[],
setSelectedItems: React.Dispatch<React.SetStateAction<string[]>>,
onSelectionItemsChange: (items: string[]) => void,
Rawallon marked this conversation as resolved.
Show resolved Hide resolved
) => {
return (
selectedItem: string | null,
downshift: ControllerStateAndHelpers<string>,
) => {
if (!selectedItem) return;
const i = selectedItems.findIndex((item) => item === selectedItem);
if (i === -1) setSelectedItems([...selectedItems, selectedItem]);
onSelectionItemsChange([...selectedItems, selectedItem]);
downshift.clearSelection();
};
},
[selectedItems],
);

const removeSelectedItemByIndex = useCallback(
(
i: number,
selectedItems: string[],
setSelectedItems: (items: string[]) => void,
onSelectionItemsChange: (items: string[]) => void,
Rawallon marked this conversation as resolved.
Show resolved Hide resolved
) => {
const temp = [...selectedItems];
temp.splice(i, 1);
setSelectedItems(temp);
onSelectionItemsChange(temp);
},
[selectedItems],
);

const getFilteredItems = useCallback(
(inputValue: string | null) => {
return items.filter(
(item) =>
!selectedItems.find((selected) => selected === item) &&
item.includes(String(inputValue)),
);
},
[selectedItems],
);

return (
<div className="relative">
Copy link
Contributor

@ribeirojose ribeirojose Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@ribeirojose ribeirojose Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From that above example ☝️ we also need an empty state when that search doesn't yield any results. Can you track that on Linear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about it, do you think we should limit number os tokens to be searched at once?

cc @luizakp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked Balancer UI and they don't handle this case as well... from all the pool type the max amount of tokens is 8. So I think it makes sense to limit 8 tokens.WDYT?

<Downshift
{...rest}
onChange={changeHandler(
selectedItems,
setSelectedItems,
onSelectionItemsChange,
)}
>
{({
getLabelProps,
getInputProps,
getItemProps,
highlightedIndex,
isOpen,
toggleMenu,
inputValue,
}) => {
return (
<div>
<div className="relative mt-1">
<div className="flex items-center bg-blue4 relative w-full cursor-default overflow-hidden rounded-lg text-left shadow-md focus:outline-none focus-within:ring-2 focus-within:ring-blue6 focus-within:ring-opacity/75 focus-within:ring-offset-2 focus-within:ring-offset-blue3 sm:text-sm">
<label {...getLabelProps()} className="flex-1 mx-1">
<MagnifyingGlassIcon />
</label>
Rawallon marked this conversation as resolved.
Show resolved Hide resolved
<div className="flex gap-2">
{selectedItems.map((value, idx) => {
return (
<Badge key={value + idx} color="blue">
<div
className="flex gap-2 items-center w-max"
Copy link
Contributor

@ribeirojose ribeirojose Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either gap-1 or adding some more horizontal padding will make this look better.
Screenshot 2023-08-31 at 11 51 59

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gap-1 would reduce the gap tho

onClick={() =>
removeSelectedItemByIndex(
idx,
selectedItems,
setSelectedItems,
onSelectionItemsChange,
)
}
>
<span>{value}</span>
<span className="cursor-pointer">
<Cross1Icon />
</span>
</div>
</Badge>
);
})}
</div>

<BaseInput
placeholder={placeholderText}
Rawallon marked this conversation as resolved.
Show resolved Hide resolved
{...getInputProps()}
onFocus={() => {
toggleMenu();
}}
onKeyDown={handleOnKeyDown}
className="hover:shadow-[0] focus:shadow-[0] shadow-none"
/>
</div>
</div>

{isOpen && (
<div className="absolute z-50 my-2 flex max-h-52 flex-col overflow-y-scroll rounded border-[1px] border-blue6 bg-blue3 scrollbar-thin scrollbar-track-blue2 scrollbar-thumb-slate12 w-full">
{getFilteredItems(inputValue).length > 0 ? (
getFilteredItems(inputValue).map((item, idx) => {
return (
<div
className={`flex w-full flex-col items-start cursor-pointer p-2 ${
idx == highlightedIndex
? "bg-blue6"
: "bg-transparent"
}`}
{...getItemProps({ item, key: item + idx })}
>
<span>{item}</span>
</div>
);
})
) : (
<div className="flex flex-col items-center text-center text-slate12 p-4">
<span>Sorry, no token was found with the symbol</span>
</div>
)}
</div>
)}
<div></div>
</div>
);
}}
</Downshift>
</div>
);
};
6 changes: 4 additions & 2 deletions apps/balancer-tools/src/app/apr/(utils)/calculatePoolStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as balEmissions from "#/lib/balancer/emissions";
import { Pool } from "#/lib/balancer/gauges";
import { pools } from "#/lib/gql/server";

import { PoolStatsData } from "../api/route";
import { PoolStatsData, PoolTokens } from "../api/route";
import { getBALPriceByRound } from "./getBALPriceByRound";
import { getPoolRelativeWeight } from "./getRelativeWeight";
import { Round } from "./rounds";
Expand All @@ -16,7 +16,7 @@ export enum PoolTypeEnum {
WEIGHTED = "Weighted",
GYROE = "GyroE",
STABLE = "Stable",
MetaStable = "MetaStable",
META_STABLE = "MetaStable",
UNKNOWN = "FX",
}

Expand Down Expand Up @@ -113,6 +113,8 @@ export async function calculatePoolStats({
votingShare,
symbol,
network,
tokens: pool.tokens as PoolTokens[],
type: pool.poolType as keyof typeof PoolTypeEnum,
};
}

Expand Down
42 changes: 42 additions & 0 deletions apps/balancer-tools/src/app/apr/(utils)/getFilteredApiUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BASE_URL } from "../api/route";
import { SearchParams } from "../round/[roundId]/page";

export const INITIAL_MIN_TVL = 1000;
export const INITIAL_LIMIT = 10;

const convert = (key: string, value: string) => {
if (["sort", "order"].includes(key)) return value || undefined;
if (["minTVL", "maxTVL", "minAPR", "maxAPR", "limit"].includes(key))
return Number(value) || undefined;
if (["tokens", "type", "network"].includes(key))
return value ? value.split(",") : undefined;
return value;
};
function getFilterDataFromParams(searchParams: SearchParams) {
const result = Object.fromEntries(
Object.entries(searchParams).map(([key, value]) => [
key,
convert(key, value),
]),
);

// Include minTVL and limit if they are not already present in searchParams
if (!("minTVL" in result)) {
result.minTVL = INITIAL_MIN_TVL;
}
if (!("limit" in result)) {
result.limit = INITIAL_LIMIT;
}

return result;
}
export default function getFilteredRoundApiUrl(
searchParams: SearchParams,
roundId: string,
) {
const filteredData = getFilterDataFromParams(searchParams);
const params = Object.entries(filteredData)
.map(([key, value]) => (value !== undefined ? `${key}=${value}` : ""))
.join("&");
return `${BASE_URL}/apr/api/?roundId=${roundId}&${params}`;
}
21 changes: 18 additions & 3 deletions apps/balancer-tools/src/app/apr/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { NextRequest, NextResponse } from "next/server";
import { Pool, POOLS_WITH_LIVE_GAUGES } from "#/lib/balancer/gauges";
import { fetcher } from "#/utils/fetcher";

import { calculatePoolStats } from "../(utils)/calculatePoolStats";
import {
calculatePoolStats,
PoolTypeEnum,
} from "../(utils)/calculatePoolStats";
import { Round } from "../(utils)/rounds";

export const BASE_URL =
Expand Down Expand Up @@ -39,6 +42,8 @@ export interface PoolStatsData extends PoolStats {
network: string;
poolId: string;
roundId: number;
tokens: PoolTokens[];
type: keyof typeof PoolTypeEnum;
}

export interface PoolStatsResults {
Expand Down Expand Up @@ -275,7 +280,8 @@ function filterPoolStats(
const maxVotingShare = parseFloat(
searchParams.get("maxVotingShare") ?? "Infinity",
);
const tokenSymbol = searchParams.get("symbol");
const tokenSymbol = searchParams.get("tokens");
const poolTypes = searchParams.get("types");
const minTvl = parseFloat(searchParams.get("minTvl") ?? "0");
const maxTvl = parseFloat(searchParams.get("maxTvl") ?? "Infinity");

Expand All @@ -295,7 +301,16 @@ function filterPoolStats(
);
}
if (tokenSymbol) {
filteredData = filteredData.filter((pool) => pool.symbol === tokenSymbol);
const decodedSymbols = tokenSymbol.split(",").map(decodeURIComponent);
filteredData = filteredData.filter((pool) =>
pool.tokens.some((token) => decodedSymbols.includes(token.symbol)),
);
}
if (poolTypes) {
const decodedSymbols = poolTypes.split(",").map(decodeURIComponent);
filteredData = filteredData.filter((pool) =>
decodedSymbols.includes(pool.type),
);
}
if (minTvl || maxTvl) {
filteredData = filteredData.filter(
Expand Down
Loading
Loading