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

2583 accounts page #2647

Open
wants to merge 18 commits into
base: 2582_landing_dashboard_page
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
488 changes: 488 additions & 0 deletions centrifuge-app/src/components/Dashboard/Account/AssetsSection.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Box, CurrencyInput, Text } from '@centrifuge/fabric'
import { Field, FieldProps } from 'formik'
import { useEffect, useRef, useState } from 'react'
import { formatBalance } from '../../../../src/utils/formatting'

export const EditableTableField = ({ name, loanId }: { name: string; loanId: string }) => {
const [isFocused, setIsFocused] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsFocused(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])

return (
<Box ref={containerRef}>
<Field name={name}>
{({ field, form }: FieldProps) =>
isFocused ? (
<CurrencyInput
{...field}
autoFocus
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
value={field.value || ''}
onChange={(value) => {
form.setFieldValue(field.name, value)
const quantity = form.values[loanId]?.quantity || 0
if (typeof value === 'number') {
form.setFieldValue(`${loanId}.newValue`, value * quantity)
}
}}
/>
) : (
<Text onClick={() => setIsFocused(true)} variant="body3">
{field.value ? formatBalance(field.value) : 0}
</Text>
)
}
</Field>
</Box>
)
}
204 changes: 204 additions & 0 deletions centrifuge-app/src/components/Dashboard/Account/OnchainSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { CurrencyBalance, Pool } from '@centrifuge/centrifuge-js'
import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Box, Button, CurrencyInput, Grid, IconChevronRight, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric'
import { useMemo, useState } from 'react'
import { PageSummary } from '../../../../src/components/PageSummary'
import { Tooltips } from '../../../../src/components/Tooltips'
import { StyledRouterTextLink } from '../../../../src/pages/Pool/Assets'
import { Dec } from '../../../../src/utils/Decimal'
import { formatBalance } from '../../../../src/utils/formatting'
import { useSuitableAccounts } from '../../../../src/utils/usePermissions'
import { useInvestorList } from '../../../../src/utils/usePools'

export default function OnchainSection({ pool }: { pool: Pool }) {
const investors = useInvestorList(pool?.id ?? '')
const [account] = useSuitableAccounts({ poolId: pool?.id ?? '', poolRole: ['LiquidityAdmin'] })
const [selectedTabIndexInvestments, setSelectedTabIndexInvestments] = useState(0)
const [selectedTabIndexRedemptions, setSelectedTabIndexRedemptions] = useState(0)
const [editMaxReserve, setEditMaxReserve] = useState(false)
const [maxReserve, setMaxReserve] = useState(pool?.reserve.max.toDecimal().toNumber())
const [error, setError] = useState<string | undefined>(undefined)
const canEditMaxReserve = !!account

const { execute: setMaxReserveTx, isLoading } = useCentrifugeTransaction(
'Set max reserve',
(cent) => cent.pools.setMaxReserve,
{
onSuccess: () => {
setEditMaxReserve(false)
setError(undefined)
},
}
)

const onClick = () => {
if (editMaxReserve) {
if (typeof maxReserve === 'number' && maxReserve >= 0) {
setMaxReserveTx([pool?.id, CurrencyBalance.fromFloat(maxReserve, pool?.currency.decimals)], { account })
} else {
setError('Invalid number')
}
} else {
setEditMaxReserve(true)
}
}

const pageSummaryData: { label: React.ReactNode; value: React.ReactNode; heading?: boolean }[] = [
{
label: <Tooltips label="Pool reserve" type="poolReserve" />,
value: <Text variant="heading1">{formatBalance(pool?.reserve.available || 0)}</Text>,
heading: false,
},
{
label: <Tooltips label="Max reserve" type="maxReserve" />,
value: editMaxReserve ? (
<CurrencyInput
value={maxReserve}
currency={pool?.currency.symbol}
onChange={(value) => setMaxReserve(value || 0)}
errorMessage={error}
/>
) : (
<Text variant="heading1">{formatBalance(pool?.reserve.max || 0)}</Text>
),
heading: false,
},
]

const pendingInvestments = useMemo(() => {
if (!pool || !pool.tranches || !investors) {
return { 0: Dec(0), 1: Dec(0) }
}

return investors.reduce(
(acc, investor) => {
const tranche = pool.tranches.find((t) => t.id === investor.trancheId)
if (tranche) {
const key = tranche.seniority === 0 ? 0 : 1
acc[key] = acc[key].add(investor.pendingInvestCurrency.toDecimal())
}
return acc
},
{ 0: Dec(0), 1: Dec(0) }
)
}, [pool, investors])

const pendingRedemptions = useMemo(() => {
if (!pool || !pool.tranches || !investors) {
return { 0: Dec(0), 1: Dec(0) }
}

return investors.reduce(
(acc, investor) => {
const tranche = pool.tranches.find((t) => t.id === investor.trancheId)
if (tranche) {
const key = tranche.seniority === 0 ? 0 : 1
acc[key] = acc[key].add(investor.pendingRedeemTrancheTokens.toDecimal())
}
return acc
},
{ 0: Dec(0), 1: Dec(0) }
)
}, [pool, investors])

const redemptions = pendingRedemptions[selectedTabIndexRedemptions as keyof typeof pendingRedemptions]
const investments = pendingInvestments[selectedTabIndexInvestments as keyof typeof pendingInvestments]

return (
<Box backgroundColor="backgroundSecondary" borderRadius={8} p={2} mt={3}>
<Box display="flex" justifyContent="space-between">
<Text variant="heading1">Onchain reserve</Text>
<StyledRouterTextLink to={`/pools/${pool?.id}/assets/0`}>
<Text variant="heading1">{formatBalance(pool?.reserve.total || 0)} USDC</Text>
<IconChevronRight />
</StyledRouterTextLink>
</Box>
<PageSummary
data={pageSummaryData}
style={{ marginLeft: 0, marginRight: 0, backgroundColor: 'white' }}
children={
<Grid gridTemplateColumns={editMaxReserve ? ['1fr 1fr'] : ['1fr']} gap={2}>
<Button
variant="secondary"
small
onClick={onClick}
disabled={
isLoading ||
!canEditMaxReserve ||
(maxReserve.toString() === pool?.reserve.max.toDecimal().toNumber().toString() && editMaxReserve)
}
>
{editMaxReserve ? 'Update' : 'Set max reserve'}
</Button>
{editMaxReserve && (
<Button variant="inverted" small onClick={() => setEditMaxReserve(false)}>
Cancel
</Button>
)}
</Grid>
}
/>
<Grid gridTemplateColumns={['1fr 1fr']} gap={2}>
<Stack
backgroundColor="backgroundPage"
borderRadius={8}
p={2}
mt={3}
border="1px solid"
borderColor="borderPrimary"
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text variant="heading4">Pending investments</Text>
<Tabs
selectedIndex={selectedTabIndexInvestments}
onChange={(index) => setSelectedTabIndexInvestments(index)}
>
{pool.tranches.map((tranche) => (
<TabsItem showBorder styleOverrides={{ padding: '8px' }}>
{tranche.seniority === 0 ? 'Junior tranche' : 'Senior tranche'}
</TabsItem>
))}
</Tabs>
</Box>
<Text
variant={investments.isZero() ? 'body2' : 'heading1'}
color={investments.isZero() ? 'textSecondary' : 'textPrimary'}
style={{ marginTop: investments.isZero() ? '12px' : 0 }}
>
{investments.isZero() ? 'No pending investments' : formatBalance(investments)}
</Text>
</Stack>
<Stack
backgroundColor="backgroundPage"
borderRadius={8}
p={2}
mt={3}
border="1px solid"
borderColor="borderPrimary"
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text variant="heading4">Pending Redemptions</Text>

<Tabs
selectedIndex={selectedTabIndexRedemptions}
onChange={(index) => setSelectedTabIndexRedemptions(index)}
>
{pool.tranches.map((tranche) => (
<TabsItem showBorder styleOverrides={{ padding: '8px' }}>
{tranche.seniority === 0 ? 'Junior tranche' : 'Senior tranche'}
</TabsItem>
))}
</Tabs>
</Box>
<Text
variant={redemptions.isZero() ? 'body2' : 'heading1'}
color={redemptions.isZero() ? 'textSecondary' : 'textPrimary'}
style={{ marginTop: redemptions.isZero() ? '12px' : 0 }}
>
{redemptions.isZero() ? 'No pending redemptions' : formatBalance(redemptions)}
</Text>
</Stack>
</Grid>
</Box>
)
}
18 changes: 15 additions & 3 deletions centrifuge-app/src/components/Dashboard/Assets/AssetsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CurrencyBalance, CurrencyMetadata, Loan, Pool } from '@centrifuge/centrifuge-js'
import { useCentrifuge } from '@centrifuge/centrifuge-react'
import { AnchorButton, Box, Button, Grid, IconDownload, IconPlus, Spinner, Text } from '@centrifuge/fabric'
import { AnchorButton, Box, Button, Grid, IconDownload, IconInfo, IconPlus, Spinner, Text } from '@centrifuge/fabric'
import { useMemo, useState } from 'react'
import styled, { useTheme } from 'styled-components'
import { useSelectedPools } from '../../..//utils/contexts/SelectedPoolsContext'
Expand Down Expand Up @@ -310,8 +310,20 @@ export function AssetsTable() {
onRowClicked={(row) => `/pools/${row.poolId}/assets/${row.assetId}`}
/>
) : (
<Box display="flex" justifyContent="center" alignItems="center" height="100%" mt={5}>
<Text variant="heading4">No data available</Text>
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
background={theme.colors.backgroundSecondary}
borderRadius={4}
p={2}
border={`1px solid ${theme.colors.borderPrimary}`}
>
<IconInfo size={14} style={{ marginRight: 8 }} />
<Text variant="body3" color="textSecondary">
No assets displayed yet
</Text>
</Box>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IconHelpCircle,
ImageUpload,
RadioButton,
Stack,
Tabs,
TabsItem,
Text,
Expand Down Expand Up @@ -82,12 +83,11 @@ export function CreateAssetsForm() {
}

return (
<Box>
<Stack flex={1} overflow="auto">
<Box
backgroundColor="backgroundSecondary"
borderRadius={8}
p={2}
mt={3}
border={`1px solid ${theme.colors.borderPrimary}`}
>
<Box>
Expand Down Expand Up @@ -236,11 +236,8 @@ export function CreateAssetsForm() {
</Box>
)}
</Box>
<Box mb={2}>
<Divider color="backgroundSecondary" />
</Box>
</>
)}
</Box>
</Stack>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@centrifuge/centrifuge-react'
import { Box, Divider, Drawer, Select } from '@centrifuge/fabric'
import { BN } from 'bn.js'
import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik'
import { Form, FormikProvider, useFormik } from 'formik'
import { useState } from 'react'
import { Navigate } from 'react-router'
import { firstValueFrom, lastValueFrom, switchMap } from 'rxjs'
Expand Down Expand Up @@ -262,38 +262,36 @@ export function CreateAssetsDrawer({ open, setOpen, type, setType }: CreateAsset
title={type === 'upload-template' ? 'Upload asset template' : 'Create asset'}
>
<Divider color="backgroundSecondary" />
<Select
name="poolId"
label="Select pool"
value={pid}
options={poolsWithMetadata?.map((pool) => ({ label: pool?.meta?.pool?.name, value: pool.id }))}
onChange={(event) => {
const selectedPool = poolsWithMetadata.find((pool) => pool.id === event.target.value) as PoolWithMetadata
form.setFieldValue('selectedPool', selectedPool)
form.setFieldValue('uploadedTemplates', selectedPool?.meta?.loanTemplates || [])
setPid(selectedPool?.id ?? '')
}}
/>

<FormikProvider value={form}>
<Form noValidate>
<Box mb={2}>
<Field name="poolId">
{({ field, form }: FieldProps) => (
<Select
name="poolId"
label="Select pool"
value={field.value}
options={poolsWithMetadata?.map((pool) => ({ label: pool?.meta?.pool?.name, value: pool.id }))}
onChange={(event) => {
const selectedPool = poolsWithMetadata.find((pool) => pool.id === event.target.value)
form.setFieldValue('selectedPool', selectedPool)
form.setFieldValue('uploadedTemplates', selectedPool?.meta?.loanTemplates || [])
setPid(selectedPool?.id ?? '')
}}
/>
)}
</Field>
<Box display="flex" flexDirection="column" height="75vh">
{type === 'create-asset' && <CreateAssetsForm />}
{type === 'upload-template' && (
<UploadAssetTemplateForm setIsUploadingTemplates={setIsUploadingTemplates} />
)}

<FooterActionButtons
type={type}
setType={setType}
setOpen={resetToDefault}
isUploadingTemplates={isUploadingTemplates}
resetToDefault={resetToDefault}
isLoading={isLoading || isTxLoading}
/>
</Box>
{type === 'create-asset' && <CreateAssetsForm />}
{type === 'upload-template' && (
<UploadAssetTemplateForm setIsUploadingTemplates={setIsUploadingTemplates} />
)}
<FooterActionButtons
type={type}
setType={setType}
setOpen={resetToDefault}
isUploadingTemplates={isUploadingTemplates}
resetToDefault={resetToDefault}
isLoading={isLoading || isTxLoading}
/>
</Form>
</FormikProvider>
</Drawer>
Expand Down
Loading
Loading