From 268cba572ca30b86f7df19c1918f4853dba8c788 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:10:19 +0200 Subject: [PATCH 1/9] prime pages --- .nvmrc | 2 +- .../src/assets/images/aave-token-logo.svg | 1 + .../src/components/DebugFlags/config.ts | 6 + .../src/components/LayoutBase/BasePadding.tsx | 2 +- .../components/LayoutBase/LayoutSection.tsx | 43 ++++++ centrifuge-app/src/components/Menu/index.tsx | 9 +- centrifuge-app/src/components/Root.tsx | 8 ++ centrifuge-app/src/pages/Prime/Detail.tsx | 15 +++ centrifuge-app/src/pages/Prime/index.tsx | 127 ++++++++++++++++++ 9 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 centrifuge-app/src/assets/images/aave-token-logo.svg create mode 100644 centrifuge-app/src/components/LayoutBase/LayoutSection.tsx create mode 100644 centrifuge-app/src/pages/Prime/Detail.tsx create mode 100644 centrifuge-app/src/pages/Prime/index.tsx diff --git a/.nvmrc b/.nvmrc index 6f7f377bf5..3f430af82b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16 +v18 diff --git a/centrifuge-app/src/assets/images/aave-token-logo.svg b/centrifuge-app/src/assets/images/aave-token-logo.svg new file mode 100644 index 0000000000..d784fbe0a4 --- /dev/null +++ b/centrifuge-app/src/assets/images/aave-token-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/centrifuge-app/src/components/DebugFlags/config.ts b/centrifuge-app/src/components/DebugFlags/config.ts index 714a67508f..f62aee2939 100644 --- a/centrifuge-app/src/components/DebugFlags/config.ts +++ b/centrifuge-app/src/components/DebugFlags/config.ts @@ -47,6 +47,7 @@ export type Key = | 'showPodAccountCreation' | 'convertEvmAddress' | 'showPortfolio' + | 'showPrime' | 'poolCreationType' export const flagsConfig: Record = { @@ -121,6 +122,11 @@ export const flagsConfig: Record = { default: false, alwaysShow: true, }, + showPrime: { + type: 'checkbox', + default: false, + alwaysShow: true, + }, poolCreationType: { type: 'select', default: config.poolCreationType || 'immediate', diff --git a/centrifuge-app/src/components/LayoutBase/BasePadding.tsx b/centrifuge-app/src/components/LayoutBase/BasePadding.tsx index 05e1dbe02a..9c9ef1b4b6 100644 --- a/centrifuge-app/src/components/LayoutBase/BasePadding.tsx +++ b/centrifuge-app/src/components/LayoutBase/BasePadding.tsx @@ -7,7 +7,7 @@ type BaseSectionProps = BoxProps & { export function BasePadding({ children, ...boxProps }: BaseSectionProps) { return ( - + {children} ) diff --git a/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx b/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx new file mode 100644 index 0000000000..237a2a1ff2 --- /dev/null +++ b/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx @@ -0,0 +1,43 @@ +import { Box, BoxProps, Shelf, Stack, Text } from '@centrifuge/fabric' +import * as React from 'react' +import { BasePadding } from './BasePadding' + +type Props = { + title?: React.ReactNode + titleAddition?: React.ReactNode + subtitle?: string + headerRight?: React.ReactNode + children: React.ReactNode +} & BoxProps + +export function LayoutSection({ title, titleAddition, subtitle, headerRight, children, ...boxProps }: Props) { + return ( + + {(title || titleAddition || subtitle || headerRight) && ( + + + {(title || titleAddition) && ( + + {title && ( + + {title} + + )} + + {titleAddition} + + + )} + {subtitle && ( + + {subtitle} + + )} + + {headerRight} + + )} + {children} + + ) +} diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index 8d7a0fdc2d..55166bc069 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -1,5 +1,6 @@ import { Box, + IconGlobe, IconInvestments, IconNft, IconPieChart, @@ -23,7 +24,7 @@ export function Menu() { const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] const isLarge = useIsAboveBreakpoint('L') const address = useAddress('substrate') - const { showPortfolio } = useDebugFlags() + const { showPortfolio, showPrime } = useDebugFlags() return ( )} + {showPrime && address && ( + + + Prime + + )} {(pools.length > 0 || config.poolCreationType === 'immediate') && ( diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx index abddbf6a8b..71a78bc70c 100644 --- a/centrifuge-app/src/components/Root.tsx +++ b/centrifuge-app/src/components/Root.tsx @@ -33,6 +33,8 @@ import { PoolDetailPage } from '../pages/Pool' import { PoolsPage } from '../pages/Pools' import { PortfolioPage } from '../pages/Portfolio' import { TransactionsPage } from '../pages/Portfolio/Transactions' +import { PrimePage } from '../pages/Prime' +import { PrimeDetailPage } from '../pages/Prime/Detail' import { TokenOverviewPage } from '../pages/Tokens' import { pinToApi } from '../utils/pinToApi' import { DebugFlags, initialFlagsState } from './DebugFlags' @@ -217,6 +219,12 @@ function Routes() { + + + + + + diff --git a/centrifuge-app/src/pages/Prime/Detail.tsx b/centrifuge-app/src/pages/Prime/Detail.tsx new file mode 100644 index 0000000000..186f823fad --- /dev/null +++ b/centrifuge-app/src/pages/Prime/Detail.tsx @@ -0,0 +1,15 @@ +import { useParams } from 'react-router' +import { LayoutBase } from '../../components/LayoutBase' + +export function PrimeDetailPage() { + return ( + + + + ) +} + +function PrimeDetail() { + const { dao } = useParams<{ dao: string }>() + return
Prime detail, {dao}
+} diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx new file mode 100644 index 0000000000..60f2beca24 --- /dev/null +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -0,0 +1,127 @@ +import { Network, useCentrifuge, useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react' +import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric' +import { useQuery } from 'react-query' +import { firstValueFrom } from 'rxjs' +import aaveLogo from '../../assets/images/aave-token-logo.svg' +import { Column, DataTable, SortableTableHeader } from '../../components/DataTable' +import { LayoutBase } from '../../components/LayoutBase' +import { LayoutSection } from '../../components/LayoutBase/LayoutSection' +import { formatBalance, formatPercentage } from '../../utils/formatting' + +type DAO = { + slug: string + name: string + network: Network + address: string + icon: string +} + +const DAOs: DAO[] = [ + { + slug: 'aave', + name: 'Aave', + network: 1, + address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f', + icon: aaveLogo, + }, +] + +export function PrimePage() { + return ( + + + + ) +} + +function Prime() { + return ( + <> + + Centrifuge Prime + + Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through + Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world and + take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs, + processes adapted to your governance, and all through decentralized rails. + + + + Go to website + + + + + + ) +} + +type Row = DAO & { value?: number; networkName: string } + +export const columns: Column[] = [ + { + align: 'left', + header: 'DAO', + cell: (row: Row) => ( + + + {row.name} + + ), + flex: '1', + }, + { + align: 'left', + header: 'Network', + cell: (row: Row) => {row.networkName}, + flex: '3', + }, + { + header: , + cell: (row: Row) => ( + {row.value && formatBalance(row.value, 'USD')} + ), + flex: '3', + sortKey: 'value', + }, + { + header: 'Profit', + cell: (row: Row) => ( + {row.value && formatPercentage(row.value)} + ), + flex: '3', + }, +] + +function DaoPortfoliosTable() { + const utils = useCentrifugeUtils() + const cent = useCentrifuge() + const getNetworkName = useGetNetworkName() + const { data } = useQuery(['daoPortfolios'], async () => { + const result = await Promise.all( + DAOs.map((dao) => { + const address = + typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address + return firstValueFrom(cent.pools.getBalances([address])) + }) + ) + return result + }) + + const rows: Row[] = DAOs.map((dao, i) => ({ + ...dao, + value: data?.[i].native.balance.toFloat(), + networkName: getNetworkName(dao.network), + })) + + return ( + + `/prime/${row.slug}`} /> + + ) +} From 6661c87f4789dd77d6db71d1d358ad75b7ad254f Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:11:01 +0200 Subject: [PATCH 2/9] table --- centrifuge-app/src/pages/Prime/index.tsx | 72 ++++++++++++------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index 60f2beca24..fb824b0f3a 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -63,41 +63,6 @@ function Prime() { type Row = DAO & { value?: number; networkName: string } -export const columns: Column[] = [ - { - align: 'left', - header: 'DAO', - cell: (row: Row) => ( - - - {row.name} - - ), - flex: '1', - }, - { - align: 'left', - header: 'Network', - cell: (row: Row) => {row.networkName}, - flex: '3', - }, - { - header: , - cell: (row: Row) => ( - {row.value && formatBalance(row.value, 'USD')} - ), - flex: '3', - sortKey: 'value', - }, - { - header: 'Profit', - cell: (row: Row) => ( - {row.value && formatPercentage(row.value)} - ), - flex: '3', - }, -] - function DaoPortfoliosTable() { const utils = useCentrifugeUtils() const cent = useCentrifuge() @@ -119,6 +84,43 @@ function DaoPortfoliosTable() { networkName: getNetworkName(dao.network), })) + const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))] + + const columns: Column[] = [ + { + align: 'left', + header: 'DAO', + cell: (row: Row) => ( + + + {row.name} + + ), + flex: '1', + }, + { + align: 'left', + header: 'Network', + cell: (row: Row) => {row.networkName}, + flex: '3', + }, + { + header: , + cell: (row: Row) => ( + {row.value && formatBalance(row.value, 'USD')} + ), + flex: '3', + sortKey: 'value', + }, + { + header: 'Profit', + cell: (row: Row) => ( + {row.value && formatPercentage(row.value)} + ), + flex: '3', + }, + ] + return ( `/prime/${row.slug}`} /> From 7786ec2392e8c3a53809adacd2314e01443931f7 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:03:41 +0200 Subject: [PATCH 3/9] filterable table header --- centrifuge-app/src/components/DataTable.tsx | 131 +++++++++++++++++- .../src/components/PoolFilter/styles.ts | 5 + centrifuge-app/src/pages/Prime/index.tsx | 25 +++- centrifuge-app/src/utils/useFilters.ts | 73 ++++++++++ fabric/src/components/FileUpload/index.tsx | 2 +- fabric/src/components/ImageUpload/index.tsx | 2 +- .../src/components/InteractiveCard/index.tsx | 2 +- .../components/Pagination/usePagination.ts | 4 +- fabric/src/index.ts | 3 +- fabric/src/utils/useControlledState.ts | 6 +- fabric/src/utils/useEventCallback.ts | 4 +- 11 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 centrifuge-app/src/utils/useFilters.ts diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index 086635ba8f..f691622456 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -1,10 +1,25 @@ -import { Card, IconArrowDown, Shelf, Stack, Text } from '@centrifuge/fabric' +import { + Box, + Card, + Checkbox, + Divider, + IconArrowDown, + IconFilter, + Menu, + Popover, + Shelf, + Stack, + Text, + Tooltip, +} from '@centrifuge/fabric' import css from '@styled-system/css' import BN from 'bn.js' import * as React from 'react' import { Link, LinkProps } from 'react-router-dom' import styled from 'styled-components' import { useElementScrollSize } from '../utils/useElementScrollSize' +import { FiltersState } from '../utils/useFilters' +import { FilterButton, QuickAction } from './PoolFilter/styles' type GroupedProps = { groupIndex?: number @@ -230,11 +245,15 @@ const HeaderCol = styled(DataCol)` align-items: center; ` -export const SortableTableHeader: React.VFC<{ label: string; orderBy?: OrderBy; align?: Column['align'] }> = ({ +export function SortableTableHeader({ label, orderBy, align, -}) => { +}: { + label: string + orderBy?: OrderBy + align?: Column['align'] +}) { return ( {(!align || align === 'right') && ( @@ -256,6 +275,112 @@ export const SortableTableHeader: React.VFC<{ label: string; orderBy?: OrderBy; ) } +export function FilterableTableHeader({ + filterKey: key, + label, + options, + filters, + align, + tooltip, +}: { + filterKey: string + label: string + options: string[] | Record + filters: FiltersState + align?: Column['align'] + tooltip?: string +}) { + const optionKeys = Array.isArray(options) ? options : Object.keys(options) + const form = React.useRef(null) + + function handleChange() { + if (!form.current) return + const formData = new FormData(form.current) + const entries = formData.getAll(key) as string[] + console.log('entries', entries) + filters.setFilter(key, entries) + } + + function deselectAll() { + filters.setFilter(key, []) + } + + function selectAll() { + filters.setFilter(key, optionKeys) + } + const state = filters.getState() + const selectedOptions = state[key] as Set | undefined + console.log('state', state) + + return ( + + { + return ( + + {tooltip ? ( + + + {label} + + + + ) : ( + + {label} + + + )} + + ) + }} + renderContent={(props, ref) => ( + + + + + + Filter {label} by: + + {optionKeys.map((option, index) => { + const label = Array.isArray(options) ? option : options[option] + const checked = filters.hasFilter(key, option) + console.log('checked', filters.hasFilter(key, option)) + return ( + + ) + })} + + + + + {selectedOptions?.size === optionKeys.length ? ( + deselectAll()}> + Deselect all + + ) : ( + selectAll()}> + Select all + + )} + + + + )} + /> + + ) +} + const StyledHeader = styled(Shelf)` color: ${({ theme }) => theme.colors.textSecondary}; diff --git a/centrifuge-app/src/components/PoolFilter/styles.ts b/centrifuge-app/src/components/PoolFilter/styles.ts index f94d6c7d85..ebc2af22df 100644 --- a/centrifuge-app/src/components/PoolFilter/styles.ts +++ b/centrifuge-app/src/components/PoolFilter/styles.ts @@ -12,6 +12,11 @@ const sharedStyles = css` outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`}; outline-offset: 4px; } + &:hover, + &:hover > svg { + cursor: pointer; + color: ${({ theme }) => theme.colors.textInteractiveHover}; + } ` export const FilterButton = styled(Text)` diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index fb824b0f3a..3394920b9c 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -3,10 +3,11 @@ import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } import { useQuery } from 'react-query' import { firstValueFrom } from 'rxjs' import aaveLogo from '../../assets/images/aave-token-logo.svg' -import { Column, DataTable, SortableTableHeader } from '../../components/DataTable' +import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from '../../components/DataTable' import { LayoutBase } from '../../components/LayoutBase' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' import { formatBalance, formatPercentage } from '../../utils/formatting' +import { useFilters } from '../../utils/useFilters' type DAO = { slug: string @@ -24,6 +25,13 @@ const DAOs: DAO[] = [ address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f', icon: aaveLogo, }, + { + slug: 'gnosis', + name: 'Gnosis', + network: 5, + address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f', + icon: aaveLogo, + }, ] export function PrimePage() { @@ -78,13 +86,15 @@ function DaoPortfoliosTable() { return result }) - const rows: Row[] = DAOs.map((dao, i) => ({ + const mapped: Row[] = DAOs.map((dao, i) => ({ ...dao, value: data?.[i].native.balance.toFloat(), networkName: getNetworkName(dao.network), })) const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))] + const filters = useFilters({ data: mapped }) + console.log('filters.data', filters.data) const columns: Column[] = [ { @@ -100,7 +110,14 @@ function DaoPortfoliosTable() { }, { align: 'left', - header: 'Network', + header: ( + [chain, getNetworkName(chain)]))} + /> + ), cell: (row: Row) => {row.networkName}, flex: '3', }, @@ -123,7 +140,7 @@ function DaoPortfoliosTable() { return ( - `/prime/${row.slug}`} /> + `/prime/${row.slug}`} /> ) } diff --git a/centrifuge-app/src/utils/useFilters.ts b/centrifuge-app/src/utils/useFilters.ts new file mode 100644 index 0000000000..cce76c3565 --- /dev/null +++ b/centrifuge-app/src/utils/useFilters.ts @@ -0,0 +1,73 @@ +import { useEventCallback } from '@centrifuge/fabric' +import get from 'lodash/get' +import * as React from 'react' +import { useHistory, useLocation } from 'react-router' + +export type PaginationProps = { + key?: string // In case more than one table on the page uses search params for filters + data?: T[] + useSearchParams?: boolean +} + +export function useFilters({ key: prefix = 'f_', data = [], useSearchParams = true }: PaginationProps = {}) { + const history = useHistory() + const { search } = useLocation() + const [params, setParams] = React.useState(() => new URLSearchParams(useSearchParams ? search : undefined)) + + const setFilter = useEventCallback((key: string, value: string[]) => { + setParams((prev) => { + const params = new URLSearchParams(prev) + params.delete(prefix + key) + value.forEach((value, i) => { + if (i === 0) { + params.set(prefix + key, value) + } else { + params.append(prefix + key, value) + } + }) + return params + }) + }) + + React.useEffect(() => { + history.replace({ search: params.toString() }) + }, [params, history]) + + const state: Record> = {} + + const hasFilter = useEventCallback((key: string, value: string) => { + return !!state[key]?.has(String(value)) + }) + + const getState = useGetLatest(state) + + for (const [prefixedKey, value] of params.entries()) { + if (!prefixedKey.startsWith(prefix)) continue + const key = prefixedKey.slice(prefix.length) + if (state[key]) { + state[key].add(value) + } else { + state[key] = new Set([value]) + } + } + + const entries = Object.entries(state) + const filtered = data.filter((entry) => entries.every(([key, set]) => set.has(String(get(entry, key))))) + + return { + setFilter, + hasFilter, + data: filtered, + state, + getState, + } +} + +export type FiltersState = ReturnType + +function useGetLatest(obj: T): () => T { + const ref = React.useRef(obj) + ref.current = obj + + return React.useState(() => () => ref.current)[0] +} diff --git a/fabric/src/components/FileUpload/index.tsx b/fabric/src/components/FileUpload/index.tsx index 297d718bec..ad403277eb 100644 --- a/fabric/src/components/FileUpload/index.tsx +++ b/fabric/src/components/FileUpload/index.tsx @@ -5,7 +5,7 @@ import IconAlertCircle from '../../icon/IconAlertCircle' import IconSpinner from '../../icon/IconSpinner' import IconUpload from '../../icon/IconUpload' import IconX from '../../icon/IconX' -import useControlledState from '../../utils/useControlledState' +import { useControlledState } from '../../utils/useControlledState' import { Box } from '../Box' import { Button } from '../Button' import { Flex } from '../Flex' diff --git a/fabric/src/components/ImageUpload/index.tsx b/fabric/src/components/ImageUpload/index.tsx index d4d4f8861e..cceac9b8f5 100644 --- a/fabric/src/components/ImageUpload/index.tsx +++ b/fabric/src/components/ImageUpload/index.tsx @@ -4,7 +4,7 @@ import { ResponsiveValue } from 'styled-system' import IconUpload from '../../icon/IconUpload' import IconX from '../../icon/IconX' import { Size } from '../../utils/types' -import useControlledState from '../../utils/useControlledState' +import { useControlledState } from '../../utils/useControlledState' import { Box } from '../Box' import { Button } from '../Button' import { Flex } from '../Flex' diff --git a/fabric/src/components/InteractiveCard/index.tsx b/fabric/src/components/InteractiveCard/index.tsx index 341776c061..b2aeaacb15 100644 --- a/fabric/src/components/InteractiveCard/index.tsx +++ b/fabric/src/components/InteractiveCard/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import styled, { useTheme } from 'styled-components' import { IconChevronRight } from '../../icon' -import useControlledState from '../../utils/useControlledState' +import { useControlledState } from '../../utils/useControlledState' import { Box } from '../Box' import { VisualButton } from '../Button' import { Card, CardProps } from '../Card' diff --git a/fabric/src/components/Pagination/usePagination.ts b/fabric/src/components/Pagination/usePagination.ts index 8936f8d404..3c80f3192c 100644 --- a/fabric/src/components/Pagination/usePagination.ts +++ b/fabric/src/components/Pagination/usePagination.ts @@ -1,6 +1,6 @@ import * as React from 'react' -import useControlledState from '../../utils/useControlledState' -import useEventCallback from '../../utils/useEventCallback' +import { useControlledState } from '../../utils/useControlledState' +import { useEventCallback } from '../../utils/useEventCallback' export type PaginationProps = { manual?: boolean diff --git a/fabric/src/index.ts b/fabric/src/index.ts index d8811ce946..303a1a3029 100644 --- a/fabric/src/index.ts +++ b/fabric/src/index.ts @@ -46,4 +46,5 @@ export * from './components/Tooltip' export * from './icon/index' export * from './theme' export { mapResponsive, toPx } from './utils/styled' -export { default as useControlledState } from './utils/useControlledState' +export { useControlledState } from './utils/useControlledState' +export { useEventCallback } from './utils/useEventCallback' diff --git a/fabric/src/utils/useControlledState.ts b/fabric/src/utils/useControlledState.ts index 6903a82590..fee802bcfa 100644 --- a/fabric/src/utils/useControlledState.ts +++ b/fabric/src/utils/useControlledState.ts @@ -1,7 +1,7 @@ import { Dispatch, SetStateAction, useState } from 'react' -import useEventCallback from './useEventCallback' +import { useEventCallback } from './useEventCallback' -function useControlledState | Dispatch>>( +export function useControlledState | Dispatch>>( initialUncontrolledValue: T, externalValue?: T, setExternalValue?: D @@ -17,5 +17,3 @@ function useControlledState | Dispatch>>( return [value, setValue as any] } - -export default useControlledState diff --git a/fabric/src/utils/useEventCallback.ts b/fabric/src/utils/useEventCallback.ts index 12904fff7c..e6a1344768 100644 --- a/fabric/src/utils/useEventCallback.ts +++ b/fabric/src/utils/useEventCallback.ts @@ -2,7 +2,7 @@ import { useCallback, useLayoutEffect, useRef } from 'react' type Calback = (...args: any[]) => any -function useEventCallback(callback: T) { +export function useEventCallback(callback: T) { const ref = useRef((() => { throw new Error('Cannot call an event handler while rendering.') }) as any) @@ -16,5 +16,3 @@ function useEventCallback(callback: T) { return fn(...args) }, []) } - -export default useEventCallback From 3f0dd3189c8c0d00df987fe05a85bed9c881f30d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:09:53 +0200 Subject: [PATCH 4/9] profit --- centrifuge-app/src/pages/Prime/index.tsx | 136 +++++++++++++++++------ centrifuge-app/src/utils/useFilters.ts | 31 +++--- centrifuge-app/src/utils/useSubquery.ts | 10 ++ 3 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 centrifuge-app/src/utils/useSubquery.ts diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index 3394920b9c..96be8fe4f7 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -1,3 +1,4 @@ +import { Price } from '@centrifuge/centrifuge-js' import { Network, useCentrifuge, useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react' import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric' import { useQuery } from 'react-query' @@ -6,8 +7,10 @@ import aaveLogo from '../../assets/images/aave-token-logo.svg' import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from '../../components/DataTable' import { LayoutBase } from '../../components/LayoutBase' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' +import { formatDate } from '../../utils/date' import { formatBalance, formatPercentage } from '../../utils/formatting' import { useFilters } from '../../utils/useFilters' +import { useSubquery } from '../../utils/useSubquery' type DAO = { slug: string @@ -21,15 +24,8 @@ const DAOs: DAO[] = [ { slug: 'aave', name: 'Aave', - network: 1, - address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f', - icon: aaveLogo, - }, - { - slug: 'gnosis', - name: 'Gnosis', - network: 5, - address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f', + network: 'centrifuge', + address: 'kALNreUp6oBmtfG87fe7MakWR8BnmQ4SmKjjfG27iVd3nuTue', icon: aaveLogo, }, ] @@ -47,12 +43,14 @@ function Prime() { <> Centrifuge Prime - - Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through - Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world and - take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs, - processes adapted to your governance, and all through decentralized rails. - + + + Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through + Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world + and take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs, + processes adapted to your governance, and all through decentralized rails. + + { - const result = await Promise.all( - DAOs.map((dao) => { - const address = - typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address - return firstValueFrom(cent.pools.getBalances([address])) - }) - ) - return result - }) - const mapped: Row[] = DAOs.map((dao, i) => ({ + const daos = DAOs.map((dao) => ({ ...dao, - value: data?.[i].native.balance.toFloat(), - networkName: getNetworkName(dao.network), + address: utils.formatAddress( + typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address + ), })) - const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))] + // TODO: Update to use new portfolio Runtime API + const { data, isLoading: isPortfoliosLoading } = useQuery(['daoPortfolios', daos.map((dao) => dao.address)], () => + Promise.all(daos.map((dao) => firstValueFrom(cent.pools.getBalances([dao.address])))) + ) + + const { data: subData, isLoading: isSubqueryLoading } = useSubquery( + `query ($accounts: [String!]) { + accounts( + filter: {id: {in: $accounts}} + ) { + nodes { + id + investorTransactions { + nodes { + timestamp + tokenPrice + tranche { + tokenPrice + trancheId + } + } + } + } + } + }`, + { + accounts: daos.map((dao) => dao.address), + } + ) + + const mapped: Row[] = daos.map((dao, i) => { + const investTxs = subData?.accounts.nodes.find((n: any) => n.id === dao.address)?.investorTransactions.nodes + const trancheBalances = + data?.[i].tranches && Object.fromEntries(data[i].tranches.map((t) => [t.trancheId, t.balance.toFloat()])) + const yields = + trancheBalances && + Object.keys(trancheBalances).map((tid) => { + const firstTx = investTxs?.find((tx: any) => tx.tranche.trancheId === tid) + const initialTokenPrice = firstTx && new Price(firstTx.tokenPrice).toFloat() + const tokenPrice = firstTx && new Price(firstTx.tranche.tokenPrice).toFloat() + const profit = tokenPrice / initialTokenPrice - 1 + return [tid, profit] as const + }) + const totalValue = trancheBalances && Object.values(trancheBalances)?.reduce((acc, balance) => acc + balance, 0) + const weightedYield = + yields && + totalValue && + yields.reduce((acc, [tid, profit]) => acc + profit * trancheBalances![tid], 0) / totalValue + + return { + ...dao, + value: totalValue ?? 0, + profit: weightedYield ? weightedYield * 100 : 0, + networkName: getNetworkName(dao.network), + firstInvestment: investTxs?.[0] && new Date(investTxs[0].timestamp), + } + }) + + const uniqueNetworks = [...new Set(daos.map((dao) => dao.network))] const filters = useFilters({ data: mapped }) - console.log('filters.data', filters.data) const columns: Column[] = [ { @@ -124,15 +170,29 @@ function DaoPortfoliosTable() { { header: , cell: (row: Row) => ( - {row.value && formatBalance(row.value, 'USD')} + + {row.value != null && formatBalance(row.value, 'USD')} + ), flex: '3', sortKey: 'value', }, { - header: 'Profit', + header: , + cell: (row: Row) => ( + + {row.profit != null && formatPercentage(row.profit)} + + ), + flex: '3', + sortKey: 'profit', + }, + { + header: 'First investment', cell: (row: Row) => ( - {row.value && formatPercentage(row.value)} + + {row.firstInvestment ? formatDate(row.firstInvestment) : '-'} + ), flex: '3', }, @@ -140,7 +200,13 @@ function DaoPortfoliosTable() { return ( - `/prime/${row.slug}`} /> + `/prime/${row.slug}`} + /> ) } diff --git a/centrifuge-app/src/utils/useFilters.ts b/centrifuge-app/src/utils/useFilters.ts index cce76c3565..5d2c23f127 100644 --- a/centrifuge-app/src/utils/useFilters.ts +++ b/centrifuge-app/src/utils/useFilters.ts @@ -13,6 +13,17 @@ export function useFilters({ key: prefix = 'f_', data = [], useSearchParams = const history = useHistory() const { search } = useLocation() const [params, setParams] = React.useState(() => new URLSearchParams(useSearchParams ? search : undefined)) + const state: Record> = {} + + for (const [prefixedKey, value] of params.entries()) { + if (!prefixedKey.startsWith(prefix)) continue + const key = prefixedKey.slice(prefix.length) + if (state[key]) { + state[key].add(value) + } else { + state[key] = new Set([value]) + } + } const setFilter = useEventCallback((key: string, value: string[]) => { setParams((prev) => { @@ -29,31 +40,19 @@ export function useFilters({ key: prefix = 'f_', data = [], useSearchParams = }) }) - React.useEffect(() => { - history.replace({ search: params.toString() }) - }, [params, history]) - - const state: Record> = {} - const hasFilter = useEventCallback((key: string, value: string) => { return !!state[key]?.has(String(value)) }) const getState = useGetLatest(state) - for (const [prefixedKey, value] of params.entries()) { - if (!prefixedKey.startsWith(prefix)) continue - const key = prefixedKey.slice(prefix.length) - if (state[key]) { - state[key].add(value) - } else { - state[key] = new Set([value]) - } - } - const entries = Object.entries(state) const filtered = data.filter((entry) => entries.every(([key, set]) => set.has(String(get(entry, key))))) + React.useEffect(() => { + history.replace({ search: params.toString() }) + }, [params, history]) + return { setFilter, hasFilter, diff --git a/centrifuge-app/src/utils/useSubquery.ts b/centrifuge-app/src/utils/useSubquery.ts new file mode 100644 index 0000000000..ed5eaf5fb5 --- /dev/null +++ b/centrifuge-app/src/utils/useSubquery.ts @@ -0,0 +1,10 @@ +import { useCentrifuge } from '@centrifuge/centrifuge-react' +import { useQuery } from 'react-query' +import { firstValueFrom } from 'rxjs' + +export function useSubquery(query: string, variables?: object) { + const cent = useCentrifuge() + return useQuery(['subquery', query, variables], () => + firstValueFrom(cent.getSubqueryObservable(query, variables, false)) + ) +} From d5a3fb8e692b9c2d0a1720392709fb8ed3245799 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:15:54 +0200 Subject: [PATCH 5/9] remove logs --- centrifuge-app/src/components/DataTable.tsx | 4 +--- centrifuge-app/src/pages/IssuerCreatePool/index.tsx | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index f691622456..5cb0169ede 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -297,7 +297,6 @@ export function FilterableTableHeader({ if (!form.current) return const formData = new FormData(form.current) const entries = formData.getAll(key) as string[] - console.log('entries', entries) filters.setFilter(key, entries) } @@ -310,7 +309,6 @@ export function FilterableTableHeader({ } const state = filters.getState() const selectedOptions = state[key] as Set | undefined - console.log('state', state) return ( @@ -346,7 +344,7 @@ export function FilterableTableHeader({ {optionKeys.map((option, index) => { const label = Array.isArray(options) ? option : options[option] const checked = filters.hasFilter(key, option) - console.log('checked', filters.hasFilter(key, option)) + return ( { const [transferToMultisig, aoProxy, adminProxy, , , , , , { adminMultisig }] = args const multisigAddr = adminMultisig && createKeyMulti(adminMultisig.signers, adminMultisig.threshold) - console.log('adminMultisig', multisigAddr) const poolArgs = args.slice(2) as any return combineLatest([ cent.getApi(), From 78f5691931b65af5b831d5e80b41a3e46293de3e Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:00:38 +0200 Subject: [PATCH 6/9] fix table --- centrifuge-app/src/components/DataTable.tsx | 9 ++++----- centrifuge-app/src/components/styles.ts | 4 ++-- centrifuge-app/src/pages/Prime/index.tsx | 7 ++----- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index a4310b8c52..3584ce2041 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -20,7 +20,8 @@ import { Link, LinkProps } from 'react-router-dom' import styled from 'styled-components' import { useElementScrollSize } from '../utils/useElementScrollSize' import { FiltersState } from '../utils/useFilters' -import { FilterButton, QuickAction } from './PoolFilter/styles' +import { FilterButton } from './FilterButton' +import { QuickAction } from './QuickAction' type GroupedProps = { groupIndex?: number @@ -275,14 +276,12 @@ export function FilterableTableHeader({ label, options, filters, - align, tooltip, }: { filterKey: string label: string options: string[] | Record filters: FiltersState - align?: Column['align'] tooltip?: string }) { const optionKeys = Array.isArray(options) ? options : Object.keys(options) @@ -316,13 +315,13 @@ export function FilterableTableHeader({ {label} - + ) : ( {label} - + )} diff --git a/centrifuge-app/src/components/styles.ts b/centrifuge-app/src/components/styles.ts index 2203668ef8..7617c1d354 100644 --- a/centrifuge-app/src/components/styles.ts +++ b/centrifuge-app/src/components/styles.ts @@ -7,8 +7,8 @@ export const buttonActionStyles = css` background-color: transparent; border-radius: ${({ theme }) => theme.radii.tooltip}px; + &:hover, &:focus-visible { - outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`}; - outline-offset: 4px; + color: ${({ theme }) => theme.colors.textInteractiveHover}; } ` diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index 96be8fe4f7..3a9672712b 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -152,7 +152,6 @@ function DaoPortfoliosTable() { {row.name}
), - flex: '1', }, { align: 'left', @@ -165,7 +164,6 @@ function DaoPortfoliosTable() { /> ), cell: (row: Row) => {row.networkName}, - flex: '3', }, { header: , @@ -174,7 +172,6 @@ function DaoPortfoliosTable() { {row.value != null && formatBalance(row.value, 'USD')} ), - flex: '3', sortKey: 'value', }, { @@ -184,17 +181,17 @@ function DaoPortfoliosTable() { {row.profit != null && formatPercentage(row.profit)} ), - flex: '3', sortKey: 'profit', }, { + align: 'left', header: 'First investment', cell: (row: Row) => ( {row.firstInvestment ? formatDate(row.firstInvestment) : '-'} ), - flex: '3', + width: '5fr', }, ] From 9416804cfc0b3381c8483540fa2e3214fc4d25ae Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:20:08 +0200 Subject: [PATCH 7/9] fix color --- centrifuge-app/src/components/DataTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx index 3584ce2041..caa3f9a0dd 100644 --- a/centrifuge-app/src/components/DataTable.tsx +++ b/centrifuge-app/src/components/DataTable.tsx @@ -374,7 +374,7 @@ export function FilterableTableHeader({ } const StyledHeader = styled(Shelf)` - color: ${({ theme }) => theme.colors.textSecondary}; + color: ${({ theme }) => theme.colors.textPrimary}; cursor: pointer; appearance: none; border: none; From 888c924ec3265d04a10da37e52b9de3431bc2205 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:21:10 +0100 Subject: [PATCH 8/9] show prime --- centrifuge-app/src/components/Menu/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index 55166bc069..63770079ce 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -54,7 +54,7 @@ export function Menu() { Portfolio )} - {showPrime && address && ( + {showPrime && ( Prime From 5396d045760d66c43c63c56b25d4ed8c03e5cda3 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:27:46 +0100 Subject: [PATCH 9/9] fix export --- centrifuge-app/src/pages/Prime/Detail.tsx | 2 +- centrifuge-app/src/pages/Prime/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centrifuge-app/src/pages/Prime/Detail.tsx b/centrifuge-app/src/pages/Prime/Detail.tsx index 186f823fad..670153f93d 100644 --- a/centrifuge-app/src/pages/Prime/Detail.tsx +++ b/centrifuge-app/src/pages/Prime/Detail.tsx @@ -1,7 +1,7 @@ import { useParams } from 'react-router' import { LayoutBase } from '../../components/LayoutBase' -export function PrimeDetailPage() { +export default function PrimeDetailPage() { return ( diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index 3a9672712b..0dd310a991 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -30,7 +30,7 @@ const DAOs: DAO[] = [ }, ] -export function PrimePage() { +export default function PrimePage() { return (