From 719c0776f57451cb05d6007a4cbdca8d4997e9d8 Mon Sep 17 00:00:00 2001
From: Katty Barroso <51223655+kattylucy@users.noreply.github.com>
Date: Mon, 7 Oct 2024 16:14:55 +0200
Subject: [PATCH 1/6] Fix gnosis chart (#2472)
Y axis values should update accordingly to show an increasing value instead of flat values
Ticks should be month only
---
.../Charts/PoolPerformanceChart.tsx | 27 ++---------
centrifuge-app/src/components/Charts/utils.ts | 26 +++++++++++
.../Portfolio/CardPortfolioValue.tsx | 8 +---
.../components/Portfolio/PortfolioValue.tsx | 46 +++++++++++--------
4 files changed, 58 insertions(+), 49 deletions(-)
diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
index ce2bc15c37..b7a2203956 100644
--- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
+++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
@@ -10,10 +10,10 @@ import { useLoans } from '../../utils/useLoans'
import { useDailyPoolStates, usePool } from '../../utils/usePools'
import { Tooltips } from '../Tooltips'
import { TooltipContainer, TooltipTitle } from './Tooltip'
-import { getRangeNumber } from './utils'
+import { getOneDayPerMonth, getRangeNumber } from './utils'
type ChartData = {
- day: Date
+ day: Date | string
nav: number
price: number | null
}
@@ -120,23 +120,6 @@ function PoolPerformanceChart() {
if (truncatedPoolStates && truncatedPoolStates?.length < 1 && poolAge > 0)
return No data available
- const getOneDayPerMonth = (): any[] => {
- const seenMonths = new Set()
- const result: any[] = []
-
- chartData.forEach((item) => {
- const date = new Date(item.day)
- const monthYear = date.toLocaleString('default', { month: 'short', year: 'numeric' })
-
- if (!seenMonths.has(monthYear)) {
- seenMonths.add(monthYear)
- result.push(item.day)
- }
- })
-
- return result
- }
-
return (
@@ -194,8 +177,8 @@ function PoolPerformanceChart() {
minTickGap={100000}
tickLine={false}
type="category"
- tick={}
- ticks={getOneDayPerMonth()}
+ tick={}
+ ticks={getOneDayPerMonth(chartData, 'day')}
/>
{
+export const CustomTick = ({ x, y, payload }: any) => {
const theme = useTheme()
return (
diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts
index f69ef6b21d..f65faeee38 100644
--- a/centrifuge-app/src/components/Charts/utils.ts
+++ b/centrifuge-app/src/components/Charts/utils.ts
@@ -1,3 +1,7 @@
+type ChartDataProps = {
+ [key: string]: any
+}
+
export const getRangeNumber = (rangeValue: string, poolAge?: number) => {
if (rangeValue === '30d') {
return 30
@@ -21,3 +25,25 @@ export const getRangeNumber = (rangeValue: string, poolAge?: number) => {
return 30
}
+
+export const getOneDayPerMonth = (chartData: ChartDataProps[], key: string): (string | number)[] => {
+ const seenMonths = new Set()
+ const result: (string | number)[] = []
+
+ chartData.forEach((item) => {
+ const value = item[key]
+ if (value) {
+ const date = new Date(value)
+ if (!isNaN(date.getTime())) {
+ const monthYear = date.toLocaleString('default', { month: 'short', year: 'numeric' })
+
+ if (!seenMonths.has(monthYear)) {
+ seenMonths.add(monthYear)
+ result.push(date.getTime())
+ }
+ }
+ }
+ })
+
+ return result
+}
diff --git a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx
index 37c2e33137..ba32d1c806 100644
--- a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx
+++ b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx
@@ -71,7 +71,7 @@ export function CardPortfolioValue({
Overview
-
+
Current portfolio value
@@ -79,12 +79,6 @@ export function CardPortfolioValue({
{formatBalance(currentPortfolioValue || 0, config.baseCurrency)}
- {/*
- Profit
-
- + {formatBalance(Dec(portfolioValue || 0), config.baseCurrency)}
-
- */}
diff --git a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx
index c723b6942a..3ad4704465 100644
--- a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx
+++ b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx
@@ -1,8 +1,10 @@
import { Card, Stack, Text } from '@centrifuge/fabric'
+import { useMemo } from 'react'
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { formatDate } from '../../utils/date'
-import { formatBalance } from '../../utils/formatting'
-import { getRangeNumber } from '../Charts/utils'
+import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting'
+import { CustomTick } from '../Charts/PoolPerformanceChart'
+import { getOneDayPerMonth, getRangeNumber } from '../Charts/utils'
import { useDailyPortfolioValue } from './usePortfolio'
const chartColor = '#006ef5'
@@ -31,17 +33,19 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad
const rangeNumber = getRangeNumber(rangeValue)
const dailyPortfolioValue = useDailyPortfolioValue(address, rangeNumber)
- const getXAxisInterval = () => {
- if (!rangeNumber) return dailyPortfolioValue ? Math.floor(dailyPortfolioValue.length / 10) : 45
- if (rangeNumber <= 30) return 5
- if (rangeNumber > 30 && rangeNumber <= 90) {
- return 14
- }
- if (rangeNumber > 90 && rangeNumber <= 180) {
- return 30
+ const chartData = dailyPortfolioValue?.map((value) => ({
+ ...value,
+ portfolioValue: value.portfolioValue.toNumber(),
+ }))
+
+ const yAxisDomain = useMemo(() => {
+ if (chartData?.length) {
+ const values = chartData.map((data) => data.portfolioValue)
+ return [Math.min(...values), Math.max(...values)]
}
- return 45
- }
+ }, [chartData])
+
+ if (!chartData?.length) return
return (
@@ -51,7 +55,7 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad
right: 20,
bottom: 0,
}}
- data={dailyPortfolioValue?.reverse()}
+ data={chartData?.reverse()}
>
@@ -60,28 +64,30 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad
-
- `${dateInMilliseconds?.toLocaleString('default', { month: 'short' })} ${dateInMilliseconds?.getDate()}`
- }
+ dataKey="dateInMilliseconds"
tickLine={false}
axisLine={false}
style={{
fontSize: '10px',
}}
dy={4}
- interval={getXAxisInterval()}
+ interval={0}
+ minTickGap={100000}
+ type="category"
+ ticks={getOneDayPerMonth(chartData, 'dateInMilliseconds')}
+ tick={}
/>
portfolioValue}
+ dataKey="portfolioValue"
tickCount={10}
tickLine={false}
axisLine={false}
- tickFormatter={(value) => value.toLocaleString()}
+ tickFormatter={(value) => formatBalanceAbbreviated(value, '', 2)}
style={{
fontSize: '10px',
}}
+ domain={yAxisDomain}
label={{
value: 'USD',
position: 'top',
From ebcfd1bfc00a0536164cb8c04b3eb31c02b06cfc Mon Sep 17 00:00:00 2001
From: Sophia
Date: Mon, 7 Oct 2024 10:53:17 -0400
Subject: [PATCH 2/6] Clean up onboarding UI after redesign (#2475)
---
.../src/components/Onboarding/Header.tsx | 22 ++-----------------
.../Onboarding/CompleteExternalOnboarding.tsx | 4 +---
.../src/pages/Onboarding/EmailVerified.tsx | 2 +-
.../pages/Onboarding/UpdateInvestorStatus.tsx | 2 +-
centrifuge-app/src/pages/Onboarding/index.tsx | 6 ++---
.../src/controllers/kyb/verifyBusiness.ts | 6 ++---
.../src/emails/sendApproveInvestorMessage.ts | 2 +-
.../src/emails/sendDocumentsMessage.ts | 6 ++---
.../src/emails/sendVerifyEmailMessage.ts | 4 ++--
9 files changed, 16 insertions(+), 38 deletions(-)
diff --git a/centrifuge-app/src/components/Onboarding/Header.tsx b/centrifuge-app/src/components/Onboarding/Header.tsx
index faadce6d37..a67b02fb63 100644
--- a/centrifuge-app/src/components/Onboarding/Header.tsx
+++ b/centrifuge-app/src/components/Onboarding/Header.tsx
@@ -1,34 +1,16 @@
-import { WalletMenu } from '@centrifuge/centrifuge-react'
-import { Box, Shelf } from '@centrifuge/fabric'
+import { Shelf } from '@centrifuge/fabric'
import * as React from 'react'
-import { Link } from 'react-router-dom'
-import { config } from '../../config'
-import { OnboardingStatus } from '../OnboardingStatus'
type HeaderProps = {
children?: React.ReactNode
- walletMenu?: boolean
}
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const [_, WordMark] = config.logo
-
-export function Header({ children, walletMenu = true }: HeaderProps) {
+export function Header({ children }: HeaderProps) {
return (
-
-
-
-
{children}
-
- {walletMenu && (
-
- ]} />
-
- )}
)
}
diff --git a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
index 48e82fdd03..cc65cacbbb 100644
--- a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
+++ b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
@@ -27,9 +27,7 @@ export const CompleteExternalOnboarding = ({ openNewTab, poolId, poolSymbol }: P
return (
-
+
<>
diff --git a/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx b/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx
index 8fd683afe8..d82796a3a8 100644
--- a/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx
+++ b/centrifuge-app/src/pages/Onboarding/EmailVerified.tsx
@@ -11,7 +11,7 @@ export default function EmailVerified() {
return (
-
+
diff --git a/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx b/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx
index e837ee8ba5..98cb13a416 100644
--- a/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx
+++ b/centrifuge-app/src/pages/Onboarding/UpdateInvestorStatus.tsx
@@ -25,7 +25,7 @@ export default function UpdateInvestorStatus() {
: null
return (
-
+
{data && poolMetadata && data && token ? (
diff --git a/centrifuge-app/src/pages/Onboarding/index.tsx b/centrifuge-app/src/pages/Onboarding/index.tsx
index 5db356fb5f..6d7171ae69 100644
--- a/centrifuge-app/src/pages/Onboarding/index.tsx
+++ b/centrifuge-app/src/pages/Onboarding/index.tsx
@@ -81,7 +81,7 @@ export default function OnboardingPage() {
}
setPool(null)
- return navigate('/onboarding')
+ return navigate('#/onboarding')
}, [
poolId,
setPool,
@@ -118,9 +118,7 @@ export default function OnboardingPage() {
return (
-
+
reference: MANUAL_KYB_REFERENCE,
email: body.email,
country: body.jurisdictionCode,
- redirect_url: `${origin}/manual-kyb-redirect.html`,
- callback_url: `${callbackBaseUrl}/manualKybCallback?${searchParams}`,
+ redirect_url: `${origin}#/manual-kyb-redirect.html`,
+ callback_url: `${callbackBaseUrl}#/manualKybCallback?${searchParams}`,
}
const manualKyb = await shuftiProRequest(payloadmanualKYB)
diff --git a/onboarding-api/src/emails/sendApproveInvestorMessage.ts b/onboarding-api/src/emails/sendApproveInvestorMessage.ts
index 28f41948d9..55e6b35caf 100644
--- a/onboarding-api/src/emails/sendApproveInvestorMessage.ts
+++ b/onboarding-api/src/emails/sendApproveInvestorMessage.ts
@@ -18,7 +18,7 @@ export const sendApproveInvestorMessage = async (
],
dynamic_template_data: {
trancheName: tranche?.currency.name,
- poolUrl: `${process.env.REDIRECT_URL}/pools/${poolId}`,
+ poolUrl: `${process.env.REDIRECT_URL}#/pools/${poolId}`,
},
},
],
diff --git a/onboarding-api/src/emails/sendDocumentsMessage.ts b/onboarding-api/src/emails/sendDocumentsMessage.ts
index e9d73fd3be..b307948d0e 100644
--- a/onboarding-api/src/emails/sendDocumentsMessage.ts
+++ b/onboarding-api/src/emails/sendDocumentsMessage.ts
@@ -54,13 +54,13 @@ export const sendDocumentsMessage = async (
},
],
dynamic_template_data: {
- rejectLink: `${process.env.REDIRECT_URL}/onboarding/updateInvestorStatus?token=${encodeURIComponent(
+ rejectLink: `${process.env.REDIRECT_URL}#/onboarding/updateInvestorStatus?token=${encodeURIComponent(
token
)}&status=rejected&metadata=${pool?.metadata}`,
- approveLink: `${process.env.REDIRECT_URL}/onboarding/updateInvestorStatus?token=${encodeURIComponent(
+ approveLink: `${process.env.REDIRECT_URL}#/onboarding/updateInvestorStatus?token=${encodeURIComponent(
token
)}&status=approved&metadata=${pool?.metadata}&network=${wallet.network}`,
- disclaimerLink: `${process.env.REDIRECT_URL}/disclaimer`,
+ disclaimerLink: `${process.env.REDIRECT_URL}#/disclaimer`,
trancheName: tranche?.currency.name,
investorEmail,
},
diff --git a/onboarding-api/src/emails/sendVerifyEmailMessage.ts b/onboarding-api/src/emails/sendVerifyEmailMessage.ts
index 8dcb06aa38..38f83309e5 100644
--- a/onboarding-api/src/emails/sendVerifyEmailMessage.ts
+++ b/onboarding-api/src/emails/sendVerifyEmailMessage.ts
@@ -26,8 +26,8 @@ export const sendVerifyEmailMessage = async (user: OnboardingUser, wallet: Reque
},
],
dynamic_template_data: {
- verifyLink: `${process.env.REDIRECT_URL}/onboarding/verifyEmail?token=${encodeURIComponent(token)}`,
- disclaimerLink: `${process.env.REDIRECT_URL}/disclaimer`,
+ verifyLink: `${process.env.REDIRECT_URL}#/onboarding/verifyEmail?token=${encodeURIComponent(token)}`,
+ disclaimerLink: `${process.env.REDIRECT_URL}#/disclaimer`,
},
},
],
From 4a888938506bc8b046984a8e5ca3b059e4913f65 Mon Sep 17 00:00:00 2001
From: Sophia
Date: Mon, 7 Oct 2024 11:43:15 -0400
Subject: [PATCH 3/6] Fix invalid character in purchase form (#2466)
* Fix invalid character when quantity is a decimal
* Fix invalid character
---
centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
index a81ce0a66f..51ec17336b 100644
--- a/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
+++ b/centrifuge-app/src/pages/Loan/ExternalFinanceForm.tsx
@@ -53,8 +53,8 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
if (source === 'reserve') {
financeTx = cent.pools.financeExternalLoan([poolId, loanId, quantity, price], { batch: true })
} else {
- const principal = new CurrencyBalance(
- price.mul(new BN(quantity.toDecimal().toString())),
+ const principal = CurrencyBalance.fromFloat(
+ price.toDecimal().mul(quantity.toDecimal()).toString(),
pool.currency.decimals
)
const repay = { principal, interest: new BN(0), unscheduled: new BN(0) }
@@ -93,7 +93,7 @@ export function ExternalFinanceForm({ loan, source }: { loan: ExternalLoan; sour
},
onSubmit: (values, actions) => {
const price = CurrencyBalance.fromFloat(values.price.toString(), pool.currency.decimals)
- const quantity = Price.fromFloat(values.quantity)
+ const quantity = Price.fromFloat(values.quantity.toString())
doFinanceTransaction([loan.poolId, loan.id, quantity, price], {
account,
})
From a183e21509a93d1563ae9bf9267c813a55d86417 Mon Sep 17 00:00:00 2001
From: Sophia
Date: Tue, 8 Oct 2024 10:39:11 -0400
Subject: [PATCH 4/6] Add ConnectionGuard to swaps fulfillment (#2476)
---
.../src/components/Swaps/Orders.tsx | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/centrifuge-app/src/components/Swaps/Orders.tsx b/centrifuge-app/src/components/Swaps/Orders.tsx
index c0ddbed895..4a8ecdb90c 100644
--- a/centrifuge-app/src/components/Swaps/Orders.tsx
+++ b/centrifuge-app/src/components/Swaps/Orders.tsx
@@ -12,6 +12,7 @@ import {
WithdrawAddress,
} from '@centrifuge/centrifuge-js'
import {
+ ConnectionGuard,
truncateAddress,
useBalances,
useCentrifuge,
@@ -110,15 +111,17 @@ export function Orders({ buyOrSell }: OrdersProps) {
align: 'left',
header: '',
cell: (row: SwapOrder) => (
-
+
+
+
),
flex: '1',
},
From 2a2ea427e87d14fae4a5fe82eb5047dedd362e40 Mon Sep 17 00:00:00 2001
From: Sophia
Date: Wed, 9 Oct 2024 13:55:59 -0400
Subject: [PATCH 5/6] Update token price of all deployed domains (#2478)
---
.../NavManagement/NavManagementAssetTable.tsx | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
index dc15c94997..792f501e4a 100644
--- a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
+++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
@@ -102,16 +102,18 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
const { execute, isLoading } = useCentrifugeTransaction(
'Update NAV',
(cent) => (args: [values: FormValues], options) => {
- const domain = domains?.find((domain) => domain.isActive && domain.hasDeployedLp)
- const updateTokenPrices = domain
- ? Object.entries(domain.liquidityPools).flatMap(([tid, poolsByCurrency]) => {
- return domain.currencies
- .filter((cur) => !!poolsByCurrency[cur.address])
- .map((cur) => [tid, cur.key] satisfies [string, CurrencyKey])
- .map(([tid, curKey]) =>
- cent.liquidityPools.updateTokenPrice([poolId, tid, curKey, domain.chainId], { batch: true })
- )
- })
+ const deployedDomains = domains?.filter((domain) => domain.hasDeployedLp)
+ const updateTokenPrices = deployedDomains
+ ? deployedDomains.flatMap((domain) =>
+ Object.entries(domain.liquidityPools).flatMap(([tid, poolsByCurrency]) => {
+ return domain.currencies
+ .filter((cur) => !!poolsByCurrency[cur.address])
+ .map((cur) => [tid, cur.key] satisfies [string, CurrencyKey])
+ .map(([tid, curKey]) =>
+ cent.liquidityPools.updateTokenPrice([poolId, tid, curKey, domain.chainId], { batch: true })
+ )
+ })
+ )
: []
return combineLatest([cent.pools.closeEpoch([poolId, false], { batch: true }), ...updateTokenPrices]).pipe(
From 180f9ec1eb664a6bbbedb80cdb19e9f8793791f8 Mon Sep 17 00:00:00 2001
From: Katty Barroso <51223655+kattylucy@users.noreply.github.com>
Date: Wed, 9 Oct 2024 20:01:36 +0200
Subject: [PATCH 6/6] Pool Overview redesign (#2435)
* Pool overview redesign
* Fix types
* Fix linter warnings
---
.../Charts/PoolPerformanceChart.tsx | 456 ++++++++++++----
.../src/components/Charts/PriceChart.tsx | 66 ++-
.../src/components/Charts/SimpleBarChart.tsx | 101 ++++
.../src/components/Charts/Tooltip.tsx | 6 +-
centrifuge-app/src/components/DataTable.tsx | 59 +-
.../components/InvestRedeem/InvestForm.tsx | 1 +
.../components/InvestRedeem/InvestRedeem.tsx | 69 +--
.../InvestRedeem/InvestRedeemDrawer.tsx | 106 ++--
.../components/InvestRedeem/RedeemForm.tsx | 3 +-
.../src/components/IssuerSection.tsx | 294 ++++++----
.../src/components/LayoutBase/index.tsx | 32 +-
.../src/components/LayoutBase/styles.tsx | 38 +-
centrifuge-app/src/components/PillButton.tsx | 11 +-
centrifuge-app/src/components/PoolList.tsx | 51 +-
.../components/PoolOverview/KeyMetrics.tsx | 359 ++++++++-----
.../PoolOverview/PoolPerfomance.tsx | 2 +-
.../components/PoolOverview/PoolStructure.tsx | 102 ----
.../PoolOverview/TrancheTokenCards.tsx | 203 ++++---
.../PoolOverview/TransactionHistory.tsx | 54 +-
.../src/components/Portfolio/Transactions.tsx | 4 +-
.../src/components/Report/BalanceSheet.tsx | 11 +-
.../components/Report/CashflowStatement.tsx | 16 +-
.../src/components/Report/DataFilter.tsx | 355 ++++++++++++
.../src/components/Report/PoolReportPage.tsx | 21 +-
.../src/components/Report/ProfitAndLoss.tsx | 19 +-
.../src/components/Report/ReportContext.tsx | 12 +-
.../src/components/Report/ReportFilter.tsx | 503 ++++++------------
.../src/components/Report/index.tsx | 21 +-
centrifuge-app/src/components/Tooltips.tsx | 16 +-
.../IssuerCreatePool/CustomCategories.tsx | 128 +++++
.../pages/IssuerCreatePool/IssuerInput.tsx | 4 +
.../src/pages/IssuerCreatePool/index.tsx | 2 +
.../pages/IssuerPool/Configuration/Issuer.tsx | 7 +-
.../src/pages/IssuerPool/Header.tsx | 3 +-
centrifuge-app/src/pages/IssuerPool/index.tsx | 2 +
.../NavManagement/NavManagementAssetTable.tsx | 2 +-
.../Onboarding/CompleteExternalOnboarding.tsx | 2 +-
centrifuge-app/src/pages/Pool/Header.tsx | 13 +-
.../src/pages/Pool/Overview/index.tsx | 124 ++---
centrifuge-app/src/pages/Pool/index.tsx | 2 +
centrifuge-app/src/pages/Pools.tsx | 17 +-
centrifuge-app/src/utils/formatting.ts | 9 +-
centrifuge-js/src/modules/pools.ts | 30 +-
fabric/.storybook/preview.jsx | 4 -
fabric/src/components/Button/WalletButton.tsx | 6 +
fabric/src/components/InputUnit/index.tsx | 31 +-
fabric/src/components/Select/index.tsx | 10 +-
fabric/src/components/Tabs/index.tsx | 40 +-
fabric/src/components/TextInput/index.tsx | 24 +-
fabric/src/components/Tooltip/index.tsx | 7 +-
fabric/src/icon-svg/Icon-balance-sheet.svg | 3 +
fabric/src/icon-svg/Icon-cashflow.svg | 3 +
fabric/src/icon-svg/Icon-profit-and-loss.svg | 3 +
fabric/src/icon-svg/IconMoody.svg | 9 +
fabric/src/icon-svg/IconSp.svg | 10 +
.../src/icon-svg/icon-arrow-right-white.svg | 10 +
fabric/src/theme/altairDark.ts | 36 --
fabric/src/theme/altairLight.ts | 28 -
fabric/src/theme/centrifugeTheme.ts | 18 +-
fabric/src/theme/index.ts | 2 -
fabric/src/theme/tokens/baseTheme.ts | 2 +-
fabric/src/theme/tokens/colors.ts | 1 +
fabric/src/theme/tokens/theme.ts | 50 +-
fabric/src/theme/tokens/typography.ts | 2 +-
64 files changed, 2215 insertions(+), 1420 deletions(-)
create mode 100644 centrifuge-app/src/components/Charts/SimpleBarChart.tsx
delete mode 100644 centrifuge-app/src/components/PoolOverview/PoolStructure.tsx
create mode 100644 centrifuge-app/src/components/Report/DataFilter.tsx
create mode 100644 centrifuge-app/src/pages/IssuerCreatePool/CustomCategories.tsx
create mode 100644 fabric/src/icon-svg/Icon-balance-sheet.svg
create mode 100644 fabric/src/icon-svg/Icon-cashflow.svg
create mode 100644 fabric/src/icon-svg/Icon-profit-and-loss.svg
create mode 100644 fabric/src/icon-svg/IconMoody.svg
create mode 100644 fabric/src/icon-svg/IconSp.svg
create mode 100644 fabric/src/icon-svg/icon-arrow-right-white.svg
delete mode 100644 fabric/src/theme/altairDark.ts
delete mode 100644 fabric/src/theme/altairLight.ts
diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
index b7a2203956..62c839d35e 100644
--- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
+++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx
@@ -1,39 +1,96 @@
-import { AnchorButton, Box, Grid, IconDownload, Shelf, Stack, Text } from '@centrifuge/fabric'
+import { DailyPoolState, DailyTrancheState, Pool } from '@centrifuge/centrifuge-js'
+import { AnchorButton, Box, IconDownload, Select, Shelf, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useParams } from 'react-router'
import { Bar, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
-import styled, { useTheme } from 'styled-components'
+import { ValueType } from 'recharts/types/component/DefaultTooltipContent'
+import { useTheme } from 'styled-components'
import { getCSVDownloadUrl } from '../../../src/utils/getCSVDownloadUrl'
import { daysBetween, formatDate } from '../../utils/date'
-import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting'
+import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting'
import { useLoans } from '../../utils/useLoans'
import { useDailyPoolStates, usePool } from '../../utils/usePools'
-import { Tooltips } from '../Tooltips'
+import { Tooltips, tooltipText } from '../Tooltips'
import { TooltipContainer, TooltipTitle } from './Tooltip'
import { getOneDayPerMonth, getRangeNumber } from './utils'
type ChartData = {
day: Date | string
nav: number
- price: number | null
+ juniorTokenPrice: number | null
+ seniorTokenPrice?: number | null
+ currency?: string
+ seniorAPY: number | null | undefined
+ juniorAPY: number | null
+ isToday: boolean
}
-const RangeFilterButton = styled(Stack)`
- &:hover {
- cursor: pointer;
+type GraphDataItemWithType = {
+ show: boolean
+ color: string
+ type: keyof typeof tooltipText
+ label: string
+ value: string | number
+}
+
+type GraphDataItemWithoutType = {
+ show: boolean
+ color: string
+ label: string
+ value: string | number
+}
+
+type GraphDataItem = GraphDataItemWithType | GraphDataItemWithoutType
+
+type CustomTickProps = {
+ x: number
+ y: number
+ payload: {
+ value: ValueType
}
-`
+}
const rangeFilters = [
+ { value: 'all', label: 'All' },
{ value: '30d', label: '30 days' },
{ value: '90d', label: '90 days' },
{ value: 'ytd', label: 'Year to date' },
- { value: 'all', label: 'All' },
-] as const
+]
+
+function calculateTranchePrices(pool: Pool) {
+ if (!pool?.tranches) return { juniorTokenPrice: 0, seniorTokenPrice: null }
+
+ const juniorTranche = pool.tranches.find((t) => t.seniority === 0)
+ const seniorTranche = pool.tranches.length > 1 ? pool.tranches.find((t) => t.seniority === 1) : null
+
+ const juniorTokenPrice =
+ juniorTranche && juniorTranche.tokenPrice ? Number(formatBalance(juniorTranche.tokenPrice, undefined, 5, 5)) : 0
+
+ const seniorTokenPrice =
+ seniorTranche && seniorTranche.tokenPrice ? Number(formatBalance(seniorTranche.tokenPrice, undefined, 5, 5)) : null
+
+ return { juniorTokenPrice, seniorTokenPrice }
+}
+
+function getYieldFieldForFilter(tranche: DailyTrancheState, filter: string) {
+ switch (filter) {
+ case '30d':
+ return tranche.yield30DaysAnnualized || 0
+ case '90d':
+ return tranche.yield90DaysAnnualized || 0
+ case 'ytd':
+ return tranche.yieldYTD || 0
+ case 'all':
+ return tranche.yieldSinceInception || 0
+ default:
+ return 0
+ }
+}
function PoolPerformanceChart() {
const theme = useTheme()
- const chartColor = theme.colors.accentPrimary
+ const [selectedTabIndex, setSelectedTabIndex] = React.useState(0)
+ const chartColor = theme.colors.textGold
const { pid: poolId } = useParams<{ pid: string }>()
if (!poolId) throw new Error('Pool not found')
@@ -51,7 +108,7 @@ function PoolPerformanceChart() {
return acc
}, '')
- const truncatedPoolStates = poolStates?.filter((poolState) => {
+ const truncatedPoolStates = poolStates?.filter((poolState: DailyPoolState) => {
if (firstOriginationDate) {
return new Date(poolState.timestamp) >= new Date(firstOriginationDate)
}
@@ -61,30 +118,67 @@ function PoolPerformanceChart() {
const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'all', label: 'All' })
const rangeNumber = getRangeNumber(range.value, poolAge) ?? 100
- const isSingleTranche = pool?.tranches.length === 1
-
// querying chain for more accurate data, since data for today from subquery is not necessarily up to date
const todayAssetValue = pool?.nav.total.toDecimal().toNumber() || 0
const todayPrice = pool?.tranches
? formatBalance(pool?.tranches[pool.tranches.length - 1].tokenPrice || 0, undefined, 5, 5)
: null
+ const trancheTodayPrice = calculateTranchePrices(pool as Pool)
+
const data: ChartData[] = React.useMemo(
() =>
truncatedPoolStates?.map((day) => {
const nav = day.poolState.netAssetValue.toDecimal().toNumber()
- const price = (isSingleTranche && Object.values(day.tranches)[0].price?.toFloat()) || null
+
+ const trancheKeys = Object.keys(day.tranches)
+ const juniorTrancheKey = trancheKeys[0]
+ const seniorTrancheKey = trancheKeys[1] || null
+
+ const juniorTokenPrice = day.tranches[juniorTrancheKey]?.price?.toFloat() ?? 0
+ const seniorTokenPrice = seniorTrancheKey ? day.tranches[seniorTrancheKey]?.price?.toFloat() ?? null : null
+
+ const juniorAPY = getYieldFieldForFilter(day.tranches[juniorTrancheKey], range.value)
+ const formattedJuniorAPY = juniorAPY !== 0 ? juniorAPY.toPercent().toNumber() : 0
+ const seniorAPY = seniorTrancheKey ? getYieldFieldForFilter(day.tranches[seniorTrancheKey], range.value) : null
+ const formattedSeniorAPY = seniorAPY !== 0 ? seniorAPY?.toPercent().toNumber() : null
+
if (day.timestamp && new Date(day.timestamp).toDateString() === new Date().toDateString()) {
- return { day: new Date(day.timestamp), nav: todayAssetValue, price: Number(todayPrice) }
+ const tranchePrices = calculateTranchePrices(pool as Pool)
+
+ return {
+ day: new Date(day.timestamp),
+ nav: todayAssetValue,
+ juniorTokenPrice: tranchePrices.juniorTokenPrice ?? 0,
+ seniorTokenPrice: tranchePrices.seniorTokenPrice ?? null,
+ juniorAPY: formattedJuniorAPY,
+ seniorAPY: formattedSeniorAPY,
+ isToday: true,
+ }
+ }
+
+ return {
+ day: new Date(day.timestamp),
+ nav: Number(nav),
+ juniorTokenPrice: juniorTokenPrice !== 0 ? juniorTokenPrice : null,
+ seniorTokenPrice: seniorTokenPrice !== 0 ? seniorTokenPrice : null,
+ juniorAPY: formattedJuniorAPY,
+ seniorAPY: formattedSeniorAPY,
+ isToday: false,
}
- return { day: new Date(day.timestamp), nav: Number(nav), price: Number(price) }
}) || [],
- [isSingleTranche, truncatedPoolStates, todayAssetValue, todayPrice]
+ [truncatedPoolStates, todayAssetValue, pool, range]
)
+ const todayData = data.find((day) => day.isToday)
+
const today = {
nav: todayAssetValue,
price: todayPrice,
+ currency: pool.currency.symbol,
+ juniorAPY: todayData?.juniorAPY,
+ seniorAPY: todayData?.seniorAPY,
+ ...trancheTodayPrice,
}
const chartData = data.slice(-rangeNumber)
@@ -94,73 +188,54 @@ function PoolPerformanceChart() {
return undefined
}
- const filteredData = chartData.map((data) => ({
- day: data.day,
- tokenPrice: data.price,
- }))
+ const filteredData = chartData.map((data) => {
+ const base = {
+ day: data.day,
+ nav: data.nav,
+ juniorTokenPrice: data.juniorTokenPrice ?? 0,
+ juniorAPY: data.juniorAPY,
+ }
+ if (data.seniorTokenPrice && data.seniorAPY) {
+ return {
+ ...base,
+ seniorTokenPrice: data.seniorTokenPrice,
+ seniorAPY: data.seniorAPY,
+ }
+ } else return { ...base }
+ })
return getCSVDownloadUrl(filteredData as any)
}, [chartData])
- const priceRange = React.useMemo(() => {
- if (!chartData) return [0, 100]
-
- const min =
- chartData?.reduce((prev, curr) => {
- return prev.price! < curr.price! ? prev : curr
- }, chartData[0])?.price || 0
-
- const max =
- chartData?.reduce((prev, curr) => {
- return prev.price! > curr.price! ? prev : curr
- }, chartData[0])?.price || 1
- return [min, max]
- }, [chartData])
-
if (truncatedPoolStates && truncatedPoolStates?.length < 1 && poolAge > 0)
return No data available
return (
-
-
-
+
+
+
Pool performance
+ setSelectedTabIndex(index)}>
+
+ Price
+
+
+ APY
+
+
Download
-
-
-
- {chartData.length > 0 &&
- rangeFilters.map((rangeFilter, index) => (
-
- setRange(rangeFilter)}>
-
- {rangeFilter.label}
-
-
-
- {index !== rangeFilters.length - 1 && (
-
- )}
-
- ))}
-
-
-
-
+
+
{chartData?.length ? (
@@ -177,13 +252,13 @@ function PoolPerformanceChart() {
minTickGap={100000}
tickLine={false}
type="category"
- tick={}
+ tick={(props) => }
ticks={getOneDayPerMonth(chartData, 'day')}
/>
formatBalanceAbbreviated(tick, '', 0)}
yAxisId="left"
width={80}
@@ -191,11 +266,11 @@ function PoolPerformanceChart() {
formatBalanceAbbreviated(tick, '', 6)}
+ style={{ fontSize: '10px', fill: theme.colors.textPrimary }}
+ tickFormatter={(tick: number) => formatBalanceAbbreviated(tick, '', 2)}
yAxisId="right"
orientation="right"
- domain={priceRange}
+ domain={selectedTabIndex === 0 ? ['dataMin - 0.25', 'dataMax + 0.25'] : [0, 'dataMax + 0.25']}
/>
{formatDate(payload[0].payload.day)}
- {payload.map(({ name, value }, index) => (
-
-
- {name === 'nav' ? 'NAV' : name === 'price' ? 'Token price' : 'Cash'}
-
-
- {name === 'nav' && typeof value === 'number'
- ? formatBalance(value, 'USD')
- : typeof value === 'number'
- ? formatBalance(value, 'USD', 6)
- : '-'}
-
-
- ))}
+ {payload.map(({ name, value }, index) => {
+ const labelMap: Record = {
+ nav: 'NAV',
+ juniorTokenPrice: 'Junior Token Price',
+ seniorTokenPrice: 'Senior Token Price',
+ juniorAPY: 'Junior APY',
+ seniorAPY: 'Senior APY',
+ default: 'Cash',
+ }
+
+ const label = typeof name === 'string' ? labelMap[name] ?? labelMap.default : labelMap.default
+
+ const formattedValue = (() => {
+ if (typeof value === 'undefined' || Array.isArray(value)) {
+ return '-'
+ }
+
+ if (name === 'juniorAPY' || name === 'seniorAPY') {
+ return formatPercentage(value)
+ }
+
+ return formatBalance(
+ Number(value),
+ name === 'nav' ? pool.currency.symbol ?? 'USD' : '',
+ name === 'juniorTokenPrice' || name === 'seniorTokenPrice' ? 6 : 0
+ )
+ })()
+
+ return (
+
+
+ {label}
+
+
+ {formattedValue}
+
+
+ )
+ })}
)
}
return null
}}
/>
-
-
+
+
+ {chartData.some((d) => d.seniorTokenPrice !== null) && (
+
+ )}
+
+
) : (
@@ -238,50 +387,131 @@ function PoolPerformanceChart() {
function CustomLegend({
data,
+ setRange,
+ selectedTabIndex,
}: {
data: {
+ currency: string
nav: number
- price: number | null
+ juniorTokenPrice: number
+ seniorTokenPrice?: number | null
+ juniorAPY: number | undefined | null
+ seniorAPY: number | undefined | null
}
+ setRange: (value: { value: string; label: string }) => void
+ selectedTabIndex: number
}) {
- const theme = useTheme()
+ const juniorAPY = data.juniorAPY ?? 0
+
+ const Dot = ({ color }: { color: string }) => (
+
+ )
+
+ const navData = {
+ color: 'backgroundTertiary',
+ label: `NAV ${data.currency}`,
+ value: formatBalance(data.nav),
+ type: 'nav',
+ show: true,
+ }
+
+ const tokenData = [
+ navData,
+ {
+ color: 'textGold',
+ label: 'Junior token price',
+ value: formatBalance(data.juniorTokenPrice ?? 0, '', 3),
+ type: 'singleTrancheTokenPrice',
+ show: true,
+ },
+ {
+ color: 'textPrimary',
+ label: 'Senior token price',
+ value: formatBalance(data.seniorTokenPrice ?? 0, '', 3),
+ type: 'singleTrancheTokenPrice',
+ show: !!data.seniorTokenPrice,
+ },
+ ]
+
+ const apyData = [
+ navData,
+ {
+ color: 'textGold',
+ label: 'Junior APY',
+ value: formatPercentage(juniorAPY),
+ show: !!data.juniorAPY,
+ },
+ {
+ color: 'textPrimary',
+ label: 'Senior APY',
+ value: formatPercentage(data.seniorAPY ?? 0),
+ show: !!data.seniorAPY,
+ },
+ ]
+
+ const graphData = selectedTabIndex === 0 ? tokenData : apyData
+
+ const toggleRange = (e: React.ChangeEvent) => {
+ const value = e.target.value
+ const range = rangeFilters.find((range) => range.value === value)
+ setRange(range ?? rangeFilters[0])
+ }
return (
-
-
-
-
- {formatBalance(data.nav, 'USD')}
-
- {data.price && (
-
-
- {data.price ? formatBalance(data.price, 'USD', 6) : '-'}
-
- )}
-
-
+
+
+ {graphData.map((item: GraphDataItem, index: number) => {
+ if (!item.show) return null
+
+ const hasType = (item: GraphDataItem): item is GraphDataItemWithType => {
+ return (item as GraphDataItemWithType).type !== undefined
+ }
+
+ return (
+
+
+
+ {hasType(item) ? (
+
+ ) : (
+
+ {item.label}
+
+ )}
+
+ {item.value}
+
+ )
+ })}
+
+
+
+
+
)
}
-export const CustomTick = ({ x, y, payload }: any) => {
+export const CustomTick = ({ x, y, payload }: CustomTickProps) => {
const theme = useTheme()
+
+ let dateValue: Date | null = null
+
+ if (payload.value instanceof Date) {
+ dateValue = payload.value
+ } else if (typeof payload.value === 'string' || typeof payload.value === 'number') {
+ dateValue = new Date(payload.value)
+ }
+
return (
- {new Date(payload.value).toLocaleString('en-US', { month: 'short' })}
+ {dateValue ? dateValue.toLocaleString('en-US', { month: 'short' }) : ''}
)
diff --git a/centrifuge-app/src/components/Charts/PriceChart.tsx b/centrifuge-app/src/components/Charts/PriceChart.tsx
index 48dde0ce78..1ccf0a62d7 100644
--- a/centrifuge-app/src/components/Charts/PriceChart.tsx
+++ b/centrifuge-app/src/components/Charts/PriceChart.tsx
@@ -1,4 +1,4 @@
-import { Box, Select, Shelf, Stack, Text } from '@centrifuge/fabric'
+import { Box, Select, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric'
import React from 'react'
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { useTheme } from 'styled-components'
@@ -8,13 +8,14 @@ import { CustomizedTooltip } from './Tooltip'
export type FilterOptions = 'YTD' | '30days' | '90days'
type PriceChartProps = {
- data: { day: Date; price: number }[]
+ data: { day: Date; price: number; apy: number }[]
currency: string
filter?: FilterOptions
setFilter?: React.Dispatch>
+ isPrice: boolean
}
-export const PriceChart = ({ data, currency, filter, setFilter }: PriceChartProps) => {
+export const PriceChart = ({ data, currency, filter, setFilter, isPrice }: PriceChartProps) => {
const theme = useTheme()
const currentPrice = data.at(-1)?.price
@@ -34,10 +35,12 @@ export const PriceChart = ({ data, currency, filter, setFilter }: PriceChartProp
)}
{priceDifference && (
-
- {' '}
- {priceDifference.gte(0) ? '+' : ''} {priceDifference.mul(100).toFixed(2)}%
-
+
+
+ {' '}
+ {priceDifference.gte(0) ? '+' : ''} {priceDifference.mul(100).toFixed(2)}%
+
+
)}
{filter && setFilter && (
@@ -50,6 +53,7 @@ export const PriceChart = ({ data, currency, filter, setFilter }: PriceChartProp
]}
onChange={(option) => setFilter(option.target.value as FilterOptions)}
defaultValue={filter}
+ hideBorder
/>
)}
@@ -57,9 +61,9 @@ export const PriceChart = ({ data, currency, filter, setFilter }: PriceChartProp
-
-
-
+
+
+
- {
- return tick.toFixed(6)
- }}
- domain={['dataMin - 0.001', 'dataMax + 0.001']}
- interval={'preserveStartEnd'}
- />
+ {isPrice ? (
+ {
+ return tick.toFixed(6)
+ }}
+ domain={['dataMin - 0.001', 'dataMax + 0.001']}
+ interval="preserveStartEnd"
+ />
+ ) : (
+ {
+ return tick.toFixed(6)
+ }}
+ domain={['dataMin - 0.001', 'dataMax + 0.001']}
+ interval="preserveStartEnd"
+ />
+ )}
} />
diff --git a/centrifuge-app/src/components/Charts/SimpleBarChart.tsx b/centrifuge-app/src/components/Charts/SimpleBarChart.tsx
new file mode 100644
index 0000000000..9d3e71e3fc
--- /dev/null
+++ b/centrifuge-app/src/components/Charts/SimpleBarChart.tsx
@@ -0,0 +1,101 @@
+import { CurrencyBalance, CurrencyMetadata } from '@centrifuge/centrifuge-js'
+import { Shelf, Text } from '@centrifuge/fabric'
+import { Bar, BarChart, CartesianGrid, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
+import { useTheme } from 'styled-components'
+import { formatDate } from '../../../src/utils/date'
+import { formatBalance, formatBalanceAbbreviated } from '../../../src/utils/formatting'
+import { LoadBoundary } from '../LoadBoundary'
+import { CustomTick } from './PoolPerformanceChart'
+import { TooltipContainer, TooltipTitle } from './Tooltip'
+
+type SimpleBarChartProps = {
+ currency?: CurrencyMetadata
+ data: { name: string; yAxis: number }[]
+}
+
+export const SimpleBarChart = ({ currency, data }: SimpleBarChartProps) => {
+ const theme = useTheme()
+
+ const getOneDayPerMonth = () => {
+ const seenMonths = new Set()
+ const result: string[] = []
+
+ data.forEach((item) => {
+ const date = new Date(item.name)
+ const month = date.getMonth() + 1
+ const year = date.getFullYear()
+ const monthYear = `${year}-${month}`
+
+ if (!seenMonths.has(monthYear)) {
+ seenMonths.add(monthYear)
+ result.push(item.name)
+ }
+ })
+
+ return result
+ }
+
+ if (!data.length)
+ return (
+
+ No data available
+
+ )
+
+ return (
+
+
+
+
+ }
+ angle={45}
+ />
+ {
+ const balance = new CurrencyBalance(tick, currency?.decimals || 0)
+ return formatBalanceAbbreviated(balance, '', 0)
+ }}
+ tick={{ fontSize: 10, color: theme.colors.textPrimary }}
+ tickLine={false}
+ axisLine={false}
+ dataKey="yAxis"
+ />
+
+
+ {
+ if (payload && payload?.length > 0) {
+ return (
+
+ {formatDate(payload[0].payload.name)}
+ {payload.map((item) => (
+ {formatBalance(item.value as number, currency)}
+ ))}
+
+ )
+ }
+ }}
+ />
+
+
+
+
+ )
+}
diff --git a/centrifuge-app/src/components/Charts/Tooltip.tsx b/centrifuge-app/src/components/Charts/Tooltip.tsx
index 37fb634801..fb179d23bb 100644
--- a/centrifuge-app/src/components/Charts/Tooltip.tsx
+++ b/centrifuge-app/src/components/Charts/Tooltip.tsx
@@ -32,9 +32,9 @@ export function TooltipContainer({ children }: { children: React.ReactNode }) {
bg="backgroundPage"
p={1}
style={{
- boxShadow: '1px 3px 6px rgba(0, 0, 0, .15)',
+ boxShadow: '1px 3px 6px 0px rgba(0, 0, 0, 0.15)',
}}
- minWidth="180px"
+ minWidth="250px"
gap="4px"
>
{children}
@@ -44,7 +44,7 @@ export function TooltipContainer({ children }: { children: React.ReactNode }) {
export function TooltipTitle({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
)
diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx
index 45088eeec4..c852234ddd 100644
--- a/centrifuge-app/src/components/DataTable.tsx
+++ b/centrifuge-app/src/components/DataTable.tsx
@@ -49,6 +49,7 @@ export type DataTableProps = {
footer?: React.ReactNode
pageSize?: number
page?: number
+ headerStyles?: React.CSSProperties
} & GroupedProps
export type OrderBy = 'asc' | 'desc'
@@ -59,6 +60,7 @@ export type Column = {
align?: string
sortKey?: string
width?: string
+ isLabel?: boolean
}
const sorter = >(data: Array, order: OrderBy, sortKey?: string) => {
if (!sortKey) return data
@@ -96,6 +98,7 @@ export const DataTable = >({
defaultSortOrder = 'desc',
pageSize = Infinity,
page = 1,
+ headerStyles,
}: DataTableProps) => {
const [orderBy, setOrderBy] = React.useState>(
defaultSortKey ? { [defaultSortKey]: defaultSortOrder } : {}
@@ -122,7 +125,7 @@ export const DataTable = >({
return (
{showHeader && (
-
+
{columns.map((col, i) => (
@@ -147,33 +150,36 @@ export const DataTable = >({
tabIndex={onRowClicked ? 0 : undefined}
>
{columns.map((col, index) => (
-
- {col.cell(row, i)}
-
- ))}
-
- ))}
- {sortedAndPaginatedData?.map((row, i) => (
-
- {columns.map((col, index) => (
-
+
{col.cell(row, i)}
))}
))}
+ {sortedAndPaginatedData?.map((row, i) => {
+ return (
+
+ {columns.map((col, index) => (
+
+ {col.cell(row, i)}
+
+ ))}
+
+ )
+ })}
{/* summary row is not included in sorting */}
{summary && (
@@ -203,12 +209,13 @@ const Row = styled('div')`
box-shadow: ${({ theme }) => `-1px 0 0 0 ${theme.colors.borderPrimary}, 1px 0 0 0 ${theme.colors.borderPrimary}`};
`
-const HeaderRow = styled(Row)(
+const HeaderRow = styled(Row)<{ styles?: any }>(({ styles }) =>
css({
backgroundColor: 'backgroundSecondary',
borderStyle: 'solid',
borderWidth: '1px 0',
borderColor: 'borderPrimary',
+ ...styles,
})
)
@@ -238,8 +245,8 @@ export const DataRow = styled(Row)`
})}
`
-export const DataCol = styled(Text)<{ align: Column['align'] }>`
- background: initial;
+export const DataCol = styled(Text)<{ align: Column['align']; isLabel?: boolean }>`
+ background: ${({ isLabel, theme }) => (isLabel ? theme.colors.backgroundSecondary : 'initial')};
border: none;
padding: 8px 16px;
display: flex;
diff --git a/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx b/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx
index c018a2b4e0..4c04252f88 100644
--- a/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx
+++ b/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx
@@ -195,6 +195,7 @@ export function InvestForm({ autoFocus, investLabel = 'Invest' }: InvestFormProp
type="submit"
loading={isInvesting}
loadingMessage={loadingMessage}
+ variant="secondary"
disabled={
state.isPoolBusy || (state.poolCurrency?.symbol.toLowerCase().includes('lp') && hasPendingOrder)
}
diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx
index 9412f5a770..d0c86a1b73 100644
--- a/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx
+++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx
@@ -16,7 +16,6 @@ import {
} from '@centrifuge/fabric'
import * as React from 'react'
import { useNavigate } from 'react-router-dom'
-import { useTheme } from 'styled-components'
import { ethConfig } from '../../config'
import { formatBalance } from '../../utils/formatting'
import { useAddress } from '../../utils/useAddress'
@@ -97,18 +96,13 @@ function InvestRedeemInput({ defaultView: defaultViewProp }: InputProps) {
if (!state.order.remainingRedeemToken.isZero()) defaultView = 'redeem'
}
const [view, setView] = React.useState<'invest' | 'redeem'>(defaultView ?? 'invest')
- const theme = useTheme()
const { data: metadata } = usePoolMetadata(pool)
return (
-
+
{renderGmp(state.poolId, state.trancheId)}
-
+
setView(index === 0 ? 'invest' : 'redeem')}
@@ -117,7 +111,7 @@ function InvestRedeemInput({ defaultView: defaultViewProp }: InputProps) {
Redeem
-
+
{state.isDataLoading ? (
) : state.isAllowedToInvest ? (
@@ -159,44 +153,27 @@ function Header() {
return (
-
- {state.trancheCurrency?.symbol} investment overview
-
+ {state.trancheCurrency?.symbol} investment overview
{connectedType && (
-
-
-
- Position
-
-
- {formatBalance(state.investmentValue, state.poolCurrency?.displayName, 2, 0)}
-
-
- {/*
-
-
- Cost basis
+
+
+ Investment position
+
+
+
+ {formatBalance(state.investmentValue, undefined, 2, 0)}
-
- -
+
+ {state.poolCurrency?.displayName}
-
-
-
-
- Profit
-
-
- -
-
- */}
-
+
+
)}
)
@@ -210,7 +187,9 @@ function Footer() {
<>
{state.actingAddress && connectedType === 'substrate' && (
- Transaction history
+
+ Transaction history
+
)}
diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx
index 1b222a8e54..b08a1d5110 100644
--- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx
+++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemDrawer.tsx
@@ -1,10 +1,15 @@
-import { Box, Drawer, Stack, Text } from '@centrifuge/fabric'
+import { DailyPoolState, Perquintill } from '@centrifuge/centrifuge-js'
+import { Box, Drawer, Stack, Tabs, TabsItem, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useDailyPoolStates, usePool } from '../../utils/usePools'
import { FilterOptions, PriceChart } from '../Charts/PriceChart'
import { LoadBoundary } from '../LoadBoundary'
import { InvestRedeem } from './InvestRedeem'
+type DailyPoolStateProps = Pick & {
+ apy?: Perquintill | undefined
+}
+
export function InvestRedeemDrawer({
poolId,
trancheId,
@@ -17,35 +22,9 @@ export function InvestRedeemDrawer({
poolId: string
trancheId: string
defaultView?: 'invest' | 'redeem'
-}) {
- return (
-
-
-
-
-
-
-
- Price
-
-
-
-
-
-
-
- )
-}
-
-const TokenPriceChart = React.memo(function TokenPriceChart({
- poolId,
- trancheId,
-}: {
- poolId: string
- trancheId: string
}) {
const [filter, setFilter] = React.useState('30days')
- const pool = usePool(poolId)
+ const [index, setIndex] = React.useState(0)
const dateFrom = React.useMemo(() => {
if (filter === 'YTD') {
@@ -72,14 +51,78 @@ const TokenPriceChart = React.memo(function TokenPriceChart({
const { poolStates: dailyPoolStates } = useDailyPoolStates(poolId, new Date(dateFrom)) || {}
+ return (
+
+
+
+
+
+ {dailyPoolStates?.length ? (
+
+
+
+ Performance
+
+ setIndex(index)}>
+
+ Price
+
+
+ APY
+
+
+
+
+
+
+ ) : null}
+
+
+ )
+}
+
+const TokenPriceChart = React.memo(function TokenPriceChart({
+ poolId,
+ trancheId,
+ dailyPoolStates,
+ filter,
+ setFilter,
+ index,
+}: {
+ poolId: string
+ trancheId: string
+ dailyPoolStates: DailyPoolStateProps[]
+ filter: FilterOptions
+ setFilter: any
+ index: number
+}) {
+ const pool = usePool(poolId)
+
const data = React.useMemo(() => {
+ const apy = {
+ '30days': 'yield30DaysAnnualized',
+ '90days': 'yield90DaysAnnualized',
+ YTD: 'yieldYTD',
+ }
const tokenData =
dailyPoolStates?.map((state) => {
- return { price: state.tranches[trancheId].price?.toFloat() || 0, day: new Date(state.timestamp) }
+ return {
+ price: state.tranches[trancheId].price?.toFloat() || 0,
+ day: new Date(state.timestamp),
+ apy: (state.tranches[trancheId] as any)[apy[filter]]?.toPercent().toNumber(),
+ }
}) || []
if (tokenData.length > 0) {
tokenData.push({
day: new Date(),
+ apy: null,
price:
pool?.tranches
.find((tranche) => tranche.id === trancheId)
@@ -88,7 +131,9 @@ const TokenPriceChart = React.memo(function TokenPriceChart({
})
}
return tokenData
- }, [dailyPoolStates, pool?.tranches, trancheId])
+ }, [dailyPoolStates, pool?.tranches, trancheId, filter])
+
+ if (!data.length) return
return (
tranche.id === trancheId)?.currency.displayName || ''}
filter={filter}
setFilter={setFilter}
+ isPrice={index === 0}
/>
)
})
diff --git a/centrifuge-app/src/components/InvestRedeem/RedeemForm.tsx b/centrifuge-app/src/components/InvestRedeem/RedeemForm.tsx
index 49752f40b4..fcec29bb2e 100644
--- a/centrifuge-app/src/components/InvestRedeem/RedeemForm.tsx
+++ b/centrifuge-app/src/components/InvestRedeem/RedeemForm.tsx
@@ -168,11 +168,12 @@ export function RedeemForm({ autoFocus }: RedeemFormProps) {
setClaimDismissed(true)} />
) : null}
{!!preSubmitAction ? (
- } />
- )}
- {report.author.title && (
- {report.author.title}} />
- )}
-
-
-
- View full analysis
-
-
- >
- )}
-
-
+ <>
+
+
+ Reports
+
+
+ View all
+
+
+
+
+ {reportLinks.map((link, i) => (
+
+
+ {link.icon}
+
+ {link.label}
+
+
+
+
+ ))}
+
+
+ {report && }
+ >
)
}
export function IssuerDetails({ metadata }: IssuerSectionProps) {
const cent = useCentrifuge()
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
+
+ const links = [
+ {
+ label: 'Website',
+ href: metadata?.pool?.links.website,
+ show: !!metadata?.pool?.links.website,
+ },
+ {
+ label: 'Forum',
+ href: metadata?.pool?.links.forum,
+ show: !!metadata?.pool?.links.forum,
+ },
+ {
+ label: 'Email',
+ href: `mailto:${metadata?.pool?.issuer.email}`,
+ show: !!metadata?.pool?.issuer.email,
+ },
+ {
+ label: 'Executive Summary',
+ show: !!metadata?.pool?.links.executiveSummary,
+ onClick: () => setIsDialogOpen(true),
+ },
+ ]
+
+ const formatCamelCase = (text: string | undefined) => {
+ if (!text) return
+ return text.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase())
+ }
+
return (
-
-
+
+
{metadata?.pool?.issuer.logo && (
)}
- {metadata?.pool?.issuer.name}} />
- {metadata?.pool?.issuer.repName}}
- />
- {metadata?.pool?.issuer.shortDescription}}
- />
- {metadata?.pool?.issuer.description}}
- />
- {metadata?.pool?.links.executiveSummary && (
-
- setIsDialogOpen(true)}>
- Executive summary
-
- setIsDialogOpen(false)}
- />
- >
- }
- />
- )}
-
-
- {(metadata?.pool?.links.website || metadata?.pool?.links.forum || metadata?.pool?.issuer.email) && (
-
-
- {metadata?.pool?.links.website && (
- Website
- )}
- {metadata?.pool?.links.forum && (
- Forum
- )}
- {metadata?.pool?.issuer.email && (
- Email
- )}
-
-
- }
- />
- )}
- {!!metadata?.pool?.details?.length && (
- } />
- )}
+
+
+
+
+ {metadata?.pool?.issuer.name}
+
+ {metadata?.pool?.issuer.description}
+
+
+ {metadata?.pool?.issuer?.categories?.length ? (
+
+ {metadata?.pool?.issuer?.categories.map((category) => (
+
+
+ {formatCamelCase(category.customType) || formatCamelCase(category.type)}
+
+
+ {category.type.includes('Rate') ? formatPercentage(category.value) : category.value}
+
+
+ ))}
+
+ ) : null}
+
+ setIsDialogOpen(false)}
+ />
)
}
+const Links = ({ links }: { links: { label: string; href?: string; show: boolean; onClick?: () => void }[] }) => {
+ return (
+
+ {links.map((link, index) => {
+ if (!link.show) return null
+
+ if (link.onClick) {
+ return (
+
+ {link.label}
+
+ )
+ }
+
+ return (
+
+ {link.label}
+
+ )
+ })}
+
+ )
+}
+
export function RatingDetails({ metadata }: IssuerSectionProps) {
const rating = metadata?.pool?.rating
- return (
+ return rating?.ratingAgency || rating?.ratingValue || rating?.ratingReportUrl ? (
Pool rating
- {rating && (
-
- {rating.ratingAgency && (
- {rating.ratingAgency}} />
- )}
- {rating.ratingValue && (
- {rating.ratingValue}} />
- )}
-
- )}
+
+ {rating.ratingAgency && (
+ {rating.ratingAgency}} />
+ )}
+ {rating.ratingValue && (
+ {rating.ratingValue}} />
+ )}
+
{rating?.ratingReportUrl && (
@@ -151,5 +214,24 @@ export function RatingDetails({ metadata }: IssuerSectionProps) {
)}
- )
+ ) : null
+}
+
+export const PoolAnalysis = ({ metadata, inverted }: IssuerSectionProps & { inverted?: boolean }) => {
+ const report = metadata?.pool?.reports?.[0]
+ return report?.author?.name || report?.author?.title ? (
+
+
+ Pool analysis
+
+
+
+ Reviewer: {report?.author?.name || 'N/A'}
+
+
+ Title: {report?.author?.title || 'N/A'}
+
+
+
+ ) : null
}
diff --git a/centrifuge-app/src/components/LayoutBase/index.tsx b/centrifuge-app/src/components/LayoutBase/index.tsx
index b8de592ca8..db5cd30888 100644
--- a/centrifuge-app/src/components/LayoutBase/index.tsx
+++ b/centrifuge-app/src/components/LayoutBase/index.tsx
@@ -12,6 +12,7 @@ import {
FooterContainer,
Inner,
LogoContainer,
+ MobileBar,
Root,
ToolbarContainer,
WalletContainer,
@@ -30,19 +31,30 @@ export function LayoutBase(): JSX.Element {
-
-
-
-
-
-
-
- {isMedium && (
+ {isMedium ? (
+
+
+
+
+
+
+
- )}
-
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
diff --git a/centrifuge-app/src/components/LayoutBase/styles.tsx b/centrifuge-app/src/components/LayoutBase/styles.tsx
index 252f54ee05..0a3be4b863 100644
--- a/centrifuge-app/src/components/LayoutBase/styles.tsx
+++ b/centrifuge-app/src/components/LayoutBase/styles.tsx
@@ -26,34 +26,51 @@ export const Root = styled(Box)`
}
}
`
-
export const Inner = styled(Grid)`
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: transparent;
- height: 100vh;
- width: 100vw;
- position: fixed;
+ position: absolute;
top: 0;
left: 0;
- overflow-y: auto;
z-index: 2;
padding-bottom: 1rem;
+ width: 100vw;
+ bottom: 0;
+ overflow-y: auto;
+
@media (min-width: ${({ theme }) => theme.breakpoints['M']}) and (max-width: ${({ theme }) =>
theme.breakpoints['L']}) {
width: 6vw;
background-color: ${({ theme }) => theme.colors.backgroundInverted};
overflow: visible;
+ height: 100vh;
}
@media (min-width: ${({ theme }) => theme.breakpoints['L']}) {
width: 15vw;
background-color: ${({ theme }) => theme.colors.backgroundInverted};
padding-left: 16px;
+ height: 100vh;
}
`
+export const MobileBar = styled(Box)`
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 3;
+ background-color: ${({ theme }) => theme.colors.backgroundInverted};
+ padding: 1rem;
+ border-top: ${({ theme }) => `1px solid ${theme.colors.borderPrimary}`};
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`
+
export const HeaderBackground = styled(Box)`
z-index: ${({ theme }) => theme.zIndices.header};
position: sticky;
@@ -75,8 +92,10 @@ export const HeaderBackground = styled(Box)`
export const LogoContainer = styled(Stack)`
background-color: ${({ theme }) => theme.colors.backgroundInverted};
z-index: ${({ theme }) => theme.zIndices.header};
- position: sticky;
+ position: absolute;
top: 0;
+ width: 100%;
+ z-index: 0;
height: ${HEADER_HEIGHT}px;
justify-content: center;
@@ -96,7 +115,7 @@ export const LogoContainer = styled(Stack)`
`
export const WalletContainer = styled(Stack)`
- position: fixed;
+ position: absolute;
top: 0;
right: 0;
z-index: ${({ theme }) => theme.zIndices.header};
@@ -106,6 +125,9 @@ export const WalletContainer = styled(Stack)`
@media (min-width: ${({ theme }) => theme.breakpoints[BREAK_POINT_COLUMNS]}) {
margin-right: 20px;
+ position: fixed;
+ top: 0;
+ right: 0;
}
`
@@ -120,11 +142,11 @@ export const WalletInner = styled(Stack)`
justify-content: center;
pointer-events: auto;
width: 250px;
- margin-right: 40px;
@media (min-width: ${({ theme }) => theme.breakpoints[BREAK_POINT_COLUMNS]}) {
justify-content: flex-end;
height: 50px;
+ margin-right: 40px;
}
`
diff --git a/centrifuge-app/src/components/PillButton.tsx b/centrifuge-app/src/components/PillButton.tsx
index 48bf81d31b..cea7e31c8e 100644
--- a/centrifuge-app/src/components/PillButton.tsx
+++ b/centrifuge-app/src/components/PillButton.tsx
@@ -8,24 +8,25 @@ const Pill = styled.button<{ variant?: 'small' | 'regular' }>(
css({
display: 'inline-block',
appearance: 'none',
- border: 0,
color: 'textPrimary',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundColor: 'backgroundSecondary',
textDecoration: 'none',
- '&:visited,&:active': {
+ borderRadius: 20,
+ '&:visited, &:active': {
color: 'textPrimary',
},
'&:hover': {
- color: 'textSelected',
+ color: 'textGold',
},
}),
- {
+ ({ theme }) => ({
+ border: `1px solid ${theme.colors.textPrimary}`,
'&:focus-visible': {
boxShadow: '3px 3px 0 var(--fabric-focus)',
},
- },
+ }),
({ variant }) =>
variant === 'regular'
? css({
diff --git a/centrifuge-app/src/components/PoolList.tsx b/centrifuge-app/src/components/PoolList.tsx
index 1b17fcc629..9ebe37ece1 100644
--- a/centrifuge-app/src/components/PoolList.tsx
+++ b/centrifuge-app/src/components/PoolList.tsx
@@ -1,6 +1,6 @@
import Centrifuge, { Pool, PoolMetadata } from '@centrifuge/centrifuge-js'
import { useCentrifuge } from '@centrifuge/centrifuge-react'
-import { Box, Shelf, Stack, Text } from '@centrifuge/fabric'
+import { Box, IconChevronRight, Shelf, Stack, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useLocation } from 'react-router'
import styled from 'styled-components'
@@ -51,7 +51,7 @@ export function PoolList() {
const sortedPools = [...openInvestmentPools, ...upcomingPools, ...tinlakePools]
return [pools, search ? filterPools([...pools, ...upcomingPools], new URLSearchParams(search)) : sortedPools]
- }, [listedPools, search])
+ }, [listedPools, search, cent, centPoolsMetaDataById])
const archivedPools = pools.filter((pool) => pool?.status?.includes('Archived'))
@@ -93,14 +93,17 @@ export function PoolList() {
{!metadataIsLoading && archivedPools.length > 0 && (
<>
- setShowArchived((show) => !show)}
- variant="body2"
- >
- {showArchived ? 'Hide archived pools' : 'View archived pools >'}
-
+
+ setShowArchived((show) => !show)}
+ variant="body2"
+ >
+ {showArchived ? 'Hide archived pools' : 'View archived pools'}
+
+ {!showArchived && }
+
{showArchived && }
>
)}
@@ -136,7 +139,6 @@ export function poolsToPoolCardProps(
cent: Centrifuge
): PoolCardProps[] {
return pools.map((pool) => {
- const tinlakePool = pool.id?.startsWith('0x') && (pool as TinlakePool)
const metaData = typeof pool.metadata === 'string' ? metaDataById[pool.id] : pool.metadata
return {
@@ -145,14 +147,7 @@ export function poolsToPoolCardProps(
assetClass: metaData?.pool?.asset.subClass,
valueLocked: getPoolValueLocked(pool),
currencySymbol: pool.currency.symbol,
- status:
- tinlakePool && tinlakePool.tinlakeMetadata.isArchived
- ? 'Archived'
- : tinlakePool && tinlakePool.addresses.CLERK !== undefined && tinlakePool.tinlakeMetadata.maker?.ilk
- ? 'Closed'
- : pool.tranches.at(0)?.capacity?.toFloat() // pool is displayed as "open for investments" if the most junior tranche has a capacity
- ? 'Open for investments'
- : ('Closed' as PoolStatusKey),
+ status: getPoolStatus(pool),
iconUri: metaData?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metaData?.pool?.icon?.uri) : undefined,
tranches: pool.tranches,
metaData: metaData as MetaData,
@@ -160,6 +155,24 @@ export function poolsToPoolCardProps(
})
}
+export function getPoolStatus(pool: Pool | TinlakePool): PoolStatusKey {
+ const tinlakePool = pool.id?.startsWith('0x') && (pool as TinlakePool)
+
+ if (tinlakePool && tinlakePool.tinlakeMetadata.isArchived) {
+ return 'Archived'
+ }
+
+ if (tinlakePool && tinlakePool.addresses.CLERK !== undefined && tinlakePool.tinlakeMetadata.maker?.ilk) {
+ return 'Closed'
+ }
+
+ if (pool.tranches.at(0)?.capacity?.toFloat()) {
+ return 'Open for investments'
+ }
+
+ return 'Closed'
+}
+
function getMetasById(pools: Pool[], poolMetas: PoolMetaDataPartial[]) {
const result: MetaDataById = {}
diff --git a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx
index e12ecb5628..9c53d4b510 100644
--- a/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx
+++ b/centrifuge-app/src/components/PoolOverview/KeyMetrics.tsx
@@ -1,52 +1,103 @@
-import { ActiveLoan, Loan, TinlakeLoan } from '@centrifuge/centrifuge-js'
-import { NetworkIcon } from '@centrifuge/centrifuge-react'
-import { Box, Card, Grid, IconExternalLink, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
+import { CurrencyBalance, DailyTrancheState, Price } from '@centrifuge/centrifuge-js'
+import { NetworkIcon, formatBalanceAbbreviated } from '@centrifuge/centrifuge-react'
+import { Box, Card, IconArrowRightWhite, IconMoody, IconSp, Shelf, Stack, Text, Tooltip } from '@centrifuge/fabric'
+import { BN } from 'bn.js'
import capitalize from 'lodash/capitalize'
import startCase from 'lodash/startCase'
+import { useMemo } from 'react'
+import { useTheme } from 'styled-components'
import { evmChains } from '../../config'
+import { formatBalance, formatPercentage } from '../../utils/formatting'
+import { useAverageMaturity } from '../../utils/useAverageMaturity'
import { useActiveDomains } from '../../utils/useLiquidityPools'
-import { usePool } from '../../utils/usePools'
+import { useDailyTranchesStates, usePool, usePoolFees, usePoolMetadata } from '../../utils/usePools'
+import { PoolStatus } from '../PoolCard/PoolStatus'
+import { getPoolStatus } from '../PoolList'
import { Spinner } from '../Spinner'
+import { Tooltips } from '../Tooltips'
type Props = {
- assetType?: { class: string; subClass: string }
- averageMaturity: string
- loans: TinlakeLoan[] | Loan[] | null | undefined
poolId: string
}
-export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props) => {
- const isTinlakePool = poolId.startsWith('0x')
+type PartialDailyTrancheState = Pick & {
+ tokenPrice: Price
+ timestamp: string
+}
- function hasValuationMethod(pricing: any): pricing is { valuationMethod: string } {
- return pricing && typeof pricing.valuationMethod === 'string'
+type DailyTrancheStateArr = Record
+
+type Tranche = Pick & {
+ currency: {
+ name: string
}
+}
- const ongoingAssetCount =
- loans &&
- [...loans].filter(
- (loan) =>
- loan.status === 'Active' &&
- hasValuationMethod(loan.pricing) &&
- loan.pricing.valuationMethod !== 'cash' &&
- !loan.outstandingDebt.isZero()
- ).length
-
- const writtenOffAssetCount =
- loans && [...loans].filter((loan) => loan.status === 'Active' && (loan as ActiveLoan).writeOffStatus).length
-
- const overdueAssetCount =
- loans &&
- [...loans].filter((loan) => {
- const today = new Date()
- today.setUTCHours(0, 0, 0, 0)
+const getTodayValue = (data: DailyTrancheStateArr | null | undefined): DailyTrancheStateArr | undefined => {
+ if (!data) return
+ if (!Object.keys(data).length) return
+
+ const today = new Date()
+
+ const filteredData: DailyTrancheStateArr = Object.keys(data).reduce((result, key) => {
+ const filteredValues = data[key].filter((obj) => {
+ const objDate = new Date(obj.timestamp)
return (
- loan.status === 'Active' &&
- loan.pricing.maturityDate &&
- new Date(loan.pricing.maturityDate).getTime() < Date.now() &&
- !loan.outstandingDebt.isZero()
+ objDate.getDate() === today.getDate() &&
+ objDate.getMonth() === today.getMonth() &&
+ objDate.getFullYear() === today.getFullYear()
)
- }).length
+ })
+
+ if (filteredValues.length > 0) {
+ result[key] = filteredValues
+ }
+
+ return result
+ }, {} as DailyTrancheStateArr)
+
+ return filteredData
+}
+
+export const KeyMetrics = ({ poolId }: Props) => {
+ const isTinlakePool = poolId.startsWith('0x')
+ const pool = usePool(poolId)
+ const { data: metadata } = usePoolMetadata(pool)
+ const poolFees = usePoolFees(poolId)
+ const tranchesIds = pool.tranches.map((tranche) => tranche.id)
+ const dailyTranches = useDailyTranchesStates(tranchesIds)
+ const totalNav = pool.nav.total.toFloat()
+ const theme = useTheme()
+ const averageMaturity = useAverageMaturity(poolId)
+
+ const pendingFees = useMemo(() => {
+ return new CurrencyBalance(
+ poolFees?.map((f) => f.amounts.pending).reduce((acc, f) => acc.add(f), new BN(0)) ?? new BN(0),
+ pool.currency.decimals
+ )
+ }, [poolFees, pool.currency.decimals])
+
+ const expenseRatio = (pendingFees.toFloat() / totalNav) * 100
+
+ const tranchesAPY = useMemo(() => {
+ const thirtyDayAPY = getTodayValue(dailyTranches)
+ if (!thirtyDayAPY) return null
+
+ return Object.keys(thirtyDayAPY).map((key) => {
+ return thirtyDayAPY[key][0].yield30DaysAnnualized
+ ? formatPercentage(thirtyDayAPY[key][0].yield30DaysAnnualized)
+ : null
+ })
+ }, [dailyTranches])
+
+ const minInvestmentPerTranche = useMemo(() => {
+ if (!metadata?.tranches) return null
+
+ return Object.values(metadata.tranches).map((item) => {
+ const minInv = new CurrencyBalance(item.minInitialInvestment ?? 0, pool.currency.decimals).toDecimal()
+ return item.minInitialInvestment ? formatBalanceAbbreviated(minInv, '', 0) : null
+ })
+ }, [metadata?.tranches, pool.currency.decimals])
const isBT3BT4 =
poolId.toLowerCase() === '0x90040f96ab8f291b6d43a8972806e977631affde' ||
@@ -54,8 +105,16 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
const metrics = [
{
- metric: 'Asset class',
- value: `${capitalize(startCase(assetType?.class))} - ${assetType?.subClass}`,
+ metric: 'Asset type',
+ value: `${capitalize(startCase(metadata?.pool?.asset?.class))} - ${metadata?.pool?.asset?.subClass}`,
+ },
+ {
+ metric: '30-day APY',
+ value: tranchesAPY?.length
+ ? tranchesAPY.map((tranche, index) => {
+ return tranche && `${tranche} ${index !== tranchesAPY?.length - 1 ? '-' : ''} `
+ })
+ : '-',
},
...(isBT3BT4
? []
@@ -66,37 +125,76 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
},
]),
{
- metric: 'Total assets',
- value:
- loans?.filter((loan) => hasValuationMethod(loan.pricing) && loan.pricing.valuationMethod !== 'cash').length ||
- 0,
- },
- {
- metric: 'Ongoing assets',
- value: ongoingAssetCount,
+ metric: 'Min. investment',
+ value: minInvestmentPerTranche?.length
+ ? minInvestmentPerTranche.map((tranche, index) => {
+ return tranche && `${tranche} ${index !== minInvestmentPerTranche?.length - 1 ? '-' : ''} `
+ })
+ : '-',
},
- ...(writtenOffAssetCount
+ ...(metadata?.pool?.investorType
? [
{
- metric: 'Written off assets',
- value: writtenOffAssetCount,
+ metric: 'Investor type',
+ value: metadata?.pool.investorType,
},
]
: []),
- ...(overdueAssetCount
+ ...(!isTinlakePool
? [
{
- metric: 'Overdue assets',
- value: overdueAssetCount,
+ metric: 'Available networks',
+ value: ,
},
]
: []),
-
- ...(!isTinlakePool
+ ...(metadata?.pool?.poolStructure
? [
{
- metric: 'Available networks',
- value: ,
+ metric: 'Pool structure',
+ value: metadata?.pool?.poolStructure,
+ },
+ ]
+ : []),
+ ...(metadata?.pool?.rating?.ratingValue
+ ? [
+ {
+ metric: 'Rating',
+ value: (
+
+ }
+ >
+
+ {metadata?.pool?.rating?.ratingAgency?.includes('moody') ? (
+
+ ) : (
+
+ )}
+ {metadata?.pool?.rating?.ratingValue}
+
+
+ ),
+ },
+ ]
+ : []),
+ ...(!!expenseRatio
+ ? [
+ {
+ metric: ,
+ value: `${formatBalance(expenseRatio * 100, '', 2)}%`,
},
]
: []),
@@ -104,31 +202,23 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
return (
-
-
- Key metrics
-
-
+
+
+
+ Overview
+
+
+
+
{metrics.map(({ metric, value }, index) => (
-
-
+
+
{metric}
{value}
-
+
))}
@@ -136,49 +226,67 @@ export const KeyMetrics = ({ assetType, averageMaturity, loans, poolId }: Props)
)
}
+const TooltipBody = ({
+ title,
+ subtitle = 'View transactions',
+ url,
+ links,
+}: {
+ title: string
+ subtitle?: string
+ url?: string
+ links?: { text: string; url: string }[]
+}) => {
+ return (
+
+
+
+ {title}
+
+ {links ? (
+ links.map((link, index) => (
+
+
+ {subtitle}
+
+
+ ))
+ ) : (
+
+
+ {subtitle}
+
+
+ )}
+
+
+
+ )
+}
+
const AvailableNetworks = ({ poolId }: { poolId: string }) => {
const activeDomains = useActiveDomains(poolId)
const pool = usePool(poolId)
+
+ const renderTooltipBody = (networkName: string, tranches: Tranche[], baseUrl: string) => {
+ const links = tranches.map((tranche) => ({
+ text: `View ${tranche.currency.name.split(' ').at(-1)}`,
+ url: `${baseUrl}/token/${tranche.id}`,
+ }))
+
+ return
+ }
+
return (
-
+
{activeDomains.data?.length || import.meta.env.REACT_APP_COLLATOR_WSS_URL.includes('development') ? (
- Centrifuge
- {pool.tranches.length > 1 ? (
- pool.tranches.map((tranche) => (
-
-
-
- View {tranche.currency.name.split(' ').at(-1)}
- {' '}
-
-
-
- ))
- ) : (
-
-
-
- View transactions
- {' '}
-
-
-
- )}
-
- }
+ body={}
>
-
+
) : (
@@ -192,41 +300,8 @@ const AvailableNetworks = ({ poolId }: { poolId: string }) => {
key={domain.poolManager}
delay={300}
bodyWidth="maxContent"
- bodyPadding={0}
- body={
-
- {chain.name}
- {pool.tranches.length > 1 ? (
- pool.tranches.map((tranche) => (
-
-
-
- View {tranche.currency.name.split(' ').at(-1)}
- {' '}
-
-
-
- ))
- ) : (
-
-
-
- View transactions
- {' '}
-
-
-
- )}
-
- }
+ bodyPadding={1}
+ body={renderTooltipBody(chain.name, pool.tranches, chain.blockExplorerUrl)}
>
diff --git a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx
index a34f086a0b..4d51641f83 100644
--- a/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx
+++ b/centrifuge-app/src/components/PoolOverview/PoolPerfomance.tsx
@@ -5,7 +5,7 @@ import { Spinner } from '../Spinner'
export const PoolPerformance = () => (
}>
-
+
diff --git a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx b/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx
deleted file mode 100644
index cb79062122..0000000000
--- a/centrifuge-app/src/components/PoolOverview/PoolStructure.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Rate } from '@centrifuge/centrifuge-js'
-import { Box, Card, Grid, Stack, Text, Tooltip } from '@centrifuge/fabric'
-import capitalize from 'lodash/capitalize'
-import { formatPercentage } from '../../utils/formatting'
-
-type Props = {
- numOfTranches: number
- poolId: string
- poolStatus?: string
- poolFees: {
- fee: Rate
- name: string
- id: number
- }[]
-}
-
-export const PoolStructure = ({ numOfTranches, poolStatus, poolFees }: Props) => {
- const metrics = [
- {
- metric: 'Pool type',
- value: capitalize(poolStatus),
- },
- {
- metric: 'Pool structure',
- value: 'Revolving pool',
- },
- {
- metric: 'Tranche structure',
- value: numOfTranches === 1 ? 'Unitranche' : `${numOfTranches} tranches`,
- },
- ...poolFees.map((fee) => {
- return {
- metric: fee.name,
- value: formatPercentage(fee.fee.toPercent(), true, {}, 3),
- }
- }),
- ]
-
- const getValue = (metric: string, value: string) => {
- if (metric === 'Pool structure')
- return (
-
-
- {value}
-
-
- )
- if (metric === 'Tranche structure')
- return (
-
-
- {value}
-
-
- )
-
- if (metric === 'Pool type' && value === 'Open')
- return (
-
-
- Open
-
-
- )
- return (
-
- {value}
-
- )
- }
-
- return (
-
-
-
- Structure
-
-
- {metrics.map(({ metric, value }, index) => (
-
-
- {metric}
-
- {getValue(metric, value)}
-
- ))}
-
-
-
- )
-}
diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx
index 9867c68f45..92db98cdfe 100644
--- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx
+++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx
@@ -1,125 +1,110 @@
-import { Box, Shelf, Stack, Text } from '@centrifuge/fabric'
+import { Perquintill } from '@centrifuge/centrifuge-js'
+import { Box, Shelf, Text } from '@centrifuge/fabric'
+import { useMemo } from 'react'
+import { useTheme } from 'styled-components'
import { InvestButton, Token } from '../../pages/Pool/Overview'
import { daysBetween } from '../../utils/date'
import { formatBalance, formatPercentage } from '../../utils/formatting'
-import { useIsAboveBreakpoint } from '../../utils/useIsAboveBreakpoint'
-import { Tooltips } from '../Tooltips'
+import { usePool } from '../../utils/usePools'
+import { DataTable } from '../DataTable'
+
+export const TrancheTokenCards = ({ trancheTokens, poolId }: { trancheTokens: Token[]; poolId: string }) => {
+ const pool = usePool(poolId)
+ const theme = useTheme()
+ const isTinlakePool = poolId.startsWith('0x')
+ const daysSinceCreation = pool?.createdAt ? daysBetween(new Date(pool.createdAt), new Date()) : 0
-export const TrancheTokenCards = ({
- trancheTokens,
- poolId,
- createdAt,
- poolCurrencySymbol,
-}: {
- trancheTokens: Token[]
- poolId: string
- createdAt: string | null
- poolCurrencySymbol: string
-}) => {
- const seniorTranche = Math.max(...trancheTokens.map((trancheToken) => trancheToken.seniority))
const getTrancheText = (trancheToken: Token) => {
- if (seniorTranche === trancheToken.seniority) return 'senior'
if (trancheToken.seniority === 0) return 'junior'
+ if (trancheToken.seniority === 1) return 'senior'
return 'mezzanine'
}
- return (
-
- {trancheTokens?.map((trancheToken) => (
-
- ))}
-
- )
-}
+ const columnConfig = useMemo(() => {
+ const calculateApy = (trancheToken: Token) => {
+ if (isTinlakePool && getTrancheText(trancheToken) === 'senior') return formatPercentage(trancheToken.apy)
+ if (daysSinceCreation < 30) return 'N/A'
+ return trancheToken.yield30DaysAnnualized
+ ? formatPercentage(new Perquintill(trancheToken.yield30DaysAnnualized))
+ : '-'
+ }
-const TrancheTokenCard = ({
- trancheToken,
- poolId,
- createdAt,
- numOfTrancheTokens,
- poolCurrencySymbol,
- trancheText,
-}: {
- trancheToken: Token
- poolId: string
- createdAt: string | null
- numOfTrancheTokens: number
- poolCurrencySymbol: string
- trancheText: 'senior' | 'junior' | 'mezzanine'
-}) => {
- const isMedium = useIsAboveBreakpoint('M')
+ return [
+ {
+ header: 'Token',
+ align: 'left',
+ formatter: (v: any) => v,
+ },
+ {
+ header: 'APY',
+ align: 'left',
+ formatter: (v: any) => (v ? calculateApy(v) : '-'),
+ },
+ {
+ header: `TVL (${pool?.currency.symbol})`,
+ align: 'left',
+ formatter: (v: any) => (v ? formatBalance(v) : '-'),
+ },
+ {
+ header: 'Token price',
+ align: 'left',
+ formatter: (v: any) => (v ? formatBalance(v, pool?.currency.symbol, pool?.currency.decimals) : '-'),
+ },
+ ...(pool.tranches.length > 1
+ ? [
+ {
+ header: 'Subordination',
+ align: 'left',
+ formatter: (_: any, row: any) => {
+ if (row.value[1].seniority === 0) return '-'
+ return formatPercentage(row.value[1].protection)
+ },
+ },
+ ]
+ : []),
+ {
+ header: '',
+ align: 'left',
+ formatter: (_: any, row: any) => {
+ return
+ },
+ },
+ ]
+ }, [pool, poolId, isTinlakePool, daysSinceCreation])
- const isTinlakePool = poolId.startsWith('0x')
- const daysSinceCreation = createdAt ? daysBetween(new Date(createdAt), new Date()) : 0
- const apyTooltipBody =
- poolId === '4139607887'
- ? 'Based on 3-month to 6-month T-Bills returns.'
- : poolId === '1655476167'
- ? 'Based on the return of the underlying funds'
- : `The 30d ${trancheText} yield is the effective annualized return of the pool's ${trancheText} token over the last 30 days.${
- daysSinceCreation < 30 && !isTinlakePool ? ' APY displayed after 30 days following token launch.' : ''
- }`
+ const columns = useMemo(() => {
+ return columnConfig.map((col, index) => {
+ return {
+ align: col.align,
+ header: col.header,
+ cell: (row: any) => (
+
+ {col.formatter(row.value[index], row)}
+
+ ),
+ }
+ })
+ }, [columnConfig])
- const calculateApy = () => {
- if (poolId === '4139607887') return formatPercentage(5)
- if (poolId === '1655476167') return formatPercentage(15)
- if (isTinlakePool && trancheText === 'senior') return formatPercentage(trancheToken.apy)
- if (daysSinceCreation < 30 || !trancheToken.yield30DaysAnnualized) return 'N/A'
- return formatPercentage(trancheToken.yield30DaysAnnualized)
- }
+ const dataTable = useMemo(() => {
+ return trancheTokens.map((tranche) => ({
+ value: [`${tranche.name} ${getTrancheText(tranche)}`, tranche, tranche.valueLocked, tranche.tokenPrice],
+ }))
+ }, [trancheTokens])
return (
-
-
-
- {trancheToken.name} ({trancheToken.symbol})
-
-
-
-
-
-
- {calculateApy()}
-
-
- {isMedium && (
-
-
- {formatPercentage(trancheToken.protection)}
-
- )}
-
-
-
- Token price
-
-
- {formatBalance(trancheToken.tokenPrice || 0, poolCurrencySymbol, 5, 5)}
-
-
-
- {formatBalance(trancheToken.valueLocked, poolCurrencySymbol)}
-
-
-
-
-
-
+
+
+
+
+
)
}
diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx
index 666a1bd8dd..3e9c0ca020 100644
--- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx
+++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx
@@ -1,5 +1,5 @@
import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js'
-import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric'
+import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric'
import BN from 'bn.js'
import { formatDate } from '../../utils/date'
import { formatBalance } from '../../utils/formatting'
@@ -24,10 +24,6 @@ type Row = {
netFlow?: 'positive' | 'negative' | 'neutral'
}
-const getTransactionTypeStatus = (type: string): 'default' | 'info' | 'ok' | 'warning' | 'critical' => {
- return 'default'
-}
-
export const TransactionHistory = ({
poolId,
activeAssetId,
@@ -198,11 +194,10 @@ export const TransactionHistoryTable = ({
const tableData =
transformedTransactions.slice(0, preview ? 8 : Infinity).map((transaction) => {
- const { label, amount, netFlow } = getLabelAndAmount(transaction)
+ const { amount, netFlow } = getLabelAndAmount(transaction)
return {
activeAssetId,
netFlow,
- type: label,
transactionDate: transaction.timestamp,
assetId: transaction.asset.id,
assetName: transaction.asset.name,
@@ -216,11 +211,6 @@ export const TransactionHistoryTable = ({
}) || []
const columns = [
- {
- align: 'left',
- header: 'Type',
- cell: ({ type }: Row) => {type},
- },
{
align: 'left',
header: ,
@@ -233,7 +223,7 @@ export const TransactionHistoryTable = ({
},
{
align: 'left',
- header: 'Asset',
+ header: ,
cell: ({ activeAssetId, assetId, assetName, fromAssetId, fromAssetName, toAssetId, toAssetName }: Row) => {
const base = `${basePath}/${poolId}/assets/`
return fromAssetId && toAssetId && activeAssetId === fromAssetId.split('-')[1] ? (
@@ -263,6 +253,7 @@ export const TransactionHistoryTable = ({
)
},
+ sortKey: 'transaction',
},
{
align: 'right',
@@ -299,25 +290,28 @@ export const TransactionHistoryTable = ({
Transaction history
- {transactions?.length && (
-
- Download
-
- )}
+
+ {transactions?.length! > 8 && preview && (
+
+ View all
+
+ )}
+ {transactions?.length && (
+
+ Download
+
+ )}
+
- {transactions?.length! > 8 && preview && (
-
- View all
-
- )}
)
}
diff --git a/centrifuge-app/src/components/Portfolio/Transactions.tsx b/centrifuge-app/src/components/Portfolio/Transactions.tsx
index 4887b7d431..653012ef0a 100644
--- a/centrifuge-app/src/components/Portfolio/Transactions.tsx
+++ b/centrifuge-app/src/components/Portfolio/Transactions.tsx
@@ -13,6 +13,7 @@ import {
usePagination,
} from '@centrifuge/fabric'
import * as React from 'react'
+import { useTheme } from 'styled-components'
import { TransactionTypeChip } from '../../components/Portfolio/TransactionTypeChip'
import { formatDate } from '../../utils/date'
import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
@@ -43,6 +44,7 @@ type Row = {
export function Transactions({ onlyMostRecent, narrow, txTypes, address, trancheId }: TransactionsProps) {
const explorer = useGetExplorerUrl()
+ const theme = useTheme()
const columns = [
{
align: 'left',
@@ -206,7 +208,7 @@ export function Transactions({ onlyMostRecent, narrow, txTypes, address, tranche
) : (
-
+
No transactions displayed yet
diff --git a/centrifuge-app/src/components/Report/BalanceSheet.tsx b/centrifuge-app/src/components/Report/BalanceSheet.tsx
index fee1b7f3b4..7f1169276f 100644
--- a/centrifuge-app/src/components/Report/BalanceSheet.tsx
+++ b/centrifuge-app/src/components/Report/BalanceSheet.tsx
@@ -20,7 +20,7 @@ type Row = TableDataRow & {
}
export function BalanceSheet({ pool }: { pool: Pool }) {
- const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext)
+ const { startDate, endDate, groupBy, setCsvData, setReportData } = React.useContext(ReportContext)
const [adjustedStartDate, adjustedEndDate] = React.useMemo(() => {
const today = new Date()
@@ -71,6 +71,7 @@ export function BalanceSheet({ pool }: { pool: Pool }) {
{row.name}
),
width: '200px',
+ isLabel: true,
},
]
.concat(
@@ -88,6 +89,7 @@ export function BalanceSheet({ pool }: { pool: Pool }) {
),
width: '170px',
+ isLabel: false,
}))
)
.concat({
@@ -95,6 +97,7 @@ export function BalanceSheet({ pool }: { pool: Pool }) {
header: '',
cell: () => ,
width: '1fr',
+ isLabel: false,
})
}, [poolStates])
@@ -239,6 +242,12 @@ export function BalanceSheet({ pool }: { pool: Pool }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetValuationRecords, trancheRecords])
+ React.useEffect(() => {
+ if (poolStates?.length) {
+ setReportData(poolStates)
+ }
+ }, [poolStates, setReportData])
+
if (!poolStates) {
return
}
diff --git a/centrifuge-app/src/components/Report/CashflowStatement.tsx b/centrifuge-app/src/components/Report/CashflowStatement.tsx
index 9647f85bd0..ca4671c126 100644
--- a/centrifuge-app/src/components/Report/CashflowStatement.tsx
+++ b/centrifuge-app/src/components/Report/CashflowStatement.tsx
@@ -1,5 +1,5 @@
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
-import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
+import { DailyPoolState, Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { formatBalance } from '@centrifuge/centrifuge-react'
import { Text, Tooltip } from '@centrifuge/fabric'
import * as React from 'react'
@@ -26,7 +26,7 @@ type Row = TableDataRow & {
}
export function CashflowStatement({ pool }: { pool: Pool }) {
- const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext)
+ const { startDate, endDate, groupBy, setCsvData, setReportData } = React.useContext(ReportContext)
const { data: poolMetadata } = usePoolMetadata(pool)
const [adjustedStartDate, adjustedEndDate] = React.useMemo(() => {
@@ -323,6 +323,18 @@ export function CashflowStatement({ pool }: { pool: Pool }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [grossCashflowRecords, netCashflowRecords])
+ React.useEffect(() => {
+ if (poolStates && Object.keys(poolStates).length > 0) {
+ const fullPoolStates: DailyPoolState[] = Object.values(poolStates).map((partialState) => {
+ return {
+ ...partialState,
+ } as DailyPoolState
+ })
+
+ setReportData(fullPoolStates)
+ }
+ }, [poolStates, setReportData])
+
if (!poolStates) {
return
}
diff --git a/centrifuge-app/src/components/Report/DataFilter.tsx b/centrifuge-app/src/components/Report/DataFilter.tsx
new file mode 100644
index 0000000000..fccfa23f5d
--- /dev/null
+++ b/centrifuge-app/src/components/Report/DataFilter.tsx
@@ -0,0 +1,355 @@
+import { Loan, Pool } from '@centrifuge/centrifuge-js'
+import { useGetNetworkName } from '@centrifuge/centrifuge-react'
+import { AnchorButton, Box, DateInput, IconDownload, SearchInput, Select, Shelf } from '@centrifuge/fabric'
+import * as React from 'react'
+import { useNavigate } from 'react-router'
+import { usePool } from '../../../src/utils/usePools'
+import { nftMetadataSchema } from '../../schemas'
+import { useBasePath } from '../../utils/useBasePath'
+import { useActiveDomains } from '../../utils/useLiquidityPools'
+import { useLoans } from '../../utils/useLoans'
+import { useMetadata } from '../../utils/useMetadata'
+import { useCentNFT } from '../../utils/useNFTs'
+import { useDebugFlags } from '../DebugFlags'
+import { GroupBy, Report, ReportContext } from './ReportContext'
+import { formatPoolFeeTransactionType } from './utils'
+
+type ReportFilterProps = {
+ poolId: string
+}
+
+export function DataFilter({ poolId }: ReportFilterProps) {
+ const {
+ csvData,
+ setStartDate,
+ startDate,
+ endDate,
+ setEndDate,
+ report,
+ loanStatus,
+ setLoanStatus,
+ txType,
+ setTxType,
+ groupBy,
+ setGroupBy,
+ activeTranche,
+ setActiveTranche,
+ address,
+ setAddress,
+ network,
+ setNetwork,
+ loan,
+ setLoan,
+ } = React.useContext(ReportContext)
+ const navigate = useNavigate()
+ const basePath = useBasePath()
+ const pool = usePool(poolId) as Pool
+
+ const { data: domains } = useActiveDomains(pool.id)
+ const getNetworkName = useGetNetworkName()
+ const loans = useLoans(pool.id) as Loan[] | undefined
+
+ const { showOracleTx } = useDebugFlags()
+
+ const reportOptions: { label: string; value: Report }[] = [
+ { label: 'Investor transactions', value: 'investor-tx' },
+ { label: 'Asset transactions', value: 'asset-tx' },
+ { label: 'Fee transactions', value: 'fee-tx' },
+ ...(showOracleTx === true ? [{ label: 'Oracle transactions', value: 'oracle-tx' as Report }] : []),
+ { label: 'Token price', value: 'token-price' },
+ { label: 'Asset list', value: 'asset-list' },
+ { label: 'Investor list', value: 'investor-list' },
+ ]
+
+ return (
+
+
+
+
+
+ {['pool-balance', 'token-price'].includes(report) && (
+
+
+ )}
+
+ {report === 'asset-list' && (
+
+
+ )}
+
+ {(report === 'investor-list' || report === 'investor-tx') && (
+
+
+ )}
+ {report === 'asset-tx' && (
+
+
+ )}
+
+ {['investor-tx', 'asset-tx', 'fee-tx'].includes(report) && (
+
+
+ )}
+ {['investor-tx', 'investor-list'].includes(report) && (
+ <>
+
+
+
+ setAddress(e.target.value)}
+ />
+
+ >
+ )}
+
+
+ {!['investor-list', 'asset-list'].includes(report) && (
+ <>
+
+ setStartDate(e.target.value)} />
+
+ setEndDate(e.target.value)} />
+ >
+ )}
+ {report === 'asset-list' && (
+ setStartDate(e.target.value)} />
+ )}
+
+ }
+ small
+ variant="inverted"
+ style={{ marginTop: 28, marginLeft: 12 }}
+ >
+ CSV
+
+
+
+ )
+}
+
+function LoanOption({ loan }: { loan: Loan }) {
+ const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, false)
+ const { data: metadata } = useMetadata(nft?.metadataUri, nftMetadataSchema)
+ return (
+
+ )
+}
diff --git a/centrifuge-app/src/components/Report/PoolReportPage.tsx b/centrifuge-app/src/components/Report/PoolReportPage.tsx
index 820f62c750..c2f0124750 100644
--- a/centrifuge-app/src/components/Report/PoolReportPage.tsx
+++ b/centrifuge-app/src/components/Report/PoolReportPage.tsx
@@ -1,34 +1,37 @@
import { Pool } from '@centrifuge/centrifuge-js'
import * as React from 'react'
-import { useParams } from 'react-router'
+import { useLocation, useParams } from 'react-router'
import { ReportComponent } from '.'
-import { usePool } from '../../utils/usePools'
+import { usePool } from '../../../src/utils/usePools'
import { LoadBoundary } from '../LoadBoundary'
import { Spinner } from '../Spinner'
+import { DataFilter } from './DataFilter'
import { ReportContextProvider } from './ReportContext'
import { ReportFilter } from './ReportFilter'
export function PoolReportPage({ header }: { header: React.ReactNode }) {
- const { pid: poolId } = useParams<{ pid: string }>()
- if (!poolId) throw new Error('Pool not found')
+ const params = useParams<{ pid: string; '*': string }>()
+ const location = useLocation()
+ const { pid: poolId } = params
- const pool = usePool(poolId) as Pool
+ if (!poolId) throw new Error('Pool not found')
return (
{header}
- {pool && }
+ {location.pathname.includes('reporting') ? : }
-
+
)
}
-function PoolDetailReporting({ pool }: { pool: Pool }) {
- if (!pool) {
+function PoolDetailReporting({ poolId }: { poolId: string }) {
+ const pool = usePool(poolId) as Pool
+ if (!poolId || !pool) {
return
}
diff --git a/centrifuge-app/src/components/Report/ProfitAndLoss.tsx b/centrifuge-app/src/components/Report/ProfitAndLoss.tsx
index cadd57ca52..c8b21bf9b3 100644
--- a/centrifuge-app/src/components/Report/ProfitAndLoss.tsx
+++ b/centrifuge-app/src/components/Report/ProfitAndLoss.tsx
@@ -1,5 +1,5 @@
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
-import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
+import { DailyPoolState, Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { formatBalance } from '@centrifuge/centrifuge-react'
import { Text, Tooltip } from '@centrifuge/fabric'
import * as React from 'react'
@@ -25,7 +25,7 @@ type Row = TableDataRow & {
}
export function ProfitAndLoss({ pool }: { pool: Pool }) {
- const { startDate, endDate, groupBy, setCsvData } = React.useContext(ReportContext)
+ const { startDate, endDate, groupBy, setCsvData, setReportData } = React.useContext(ReportContext)
const { data: poolMetadata } = usePoolMetadata(pool)
const [adjustedStartDate, adjustedEndDate] = React.useMemo(() => {
@@ -89,6 +89,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) {
{row.name}
),
width: '240px',
+ isLabel: true,
},
]
.concat(
@@ -102,6 +103,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) {
),
width: '170px',
+ isLabel: false,
}))
)
.concat({
@@ -109,6 +111,7 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) {
header: '',
cell: () => ,
width: '1fr',
+ isLabel: false,
})
}, [poolStates, groupBy])
@@ -334,6 +337,18 @@ export function ProfitAndLoss({ pool }: { pool: Pool }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [profitAndLossRecords, feesRecords, totalProfitRecords])
+ React.useEffect(() => {
+ if (poolStates && Object.keys(poolStates).length > 0) {
+ const fullPoolStates: DailyPoolState[] = Object.values(poolStates).map((partialState) => {
+ return {
+ ...partialState,
+ } as DailyPoolState
+ })
+
+ setReportData(fullPoolStates)
+ }
+ }, [poolStates, setReportData])
+
if (!poolStates) {
return
}
diff --git a/centrifuge-app/src/components/Report/ReportContext.tsx b/centrifuge-app/src/components/Report/ReportContext.tsx
index e9a9876c1c..7edcb7ed7a 100644
--- a/centrifuge-app/src/components/Report/ReportContext.tsx
+++ b/centrifuge-app/src/components/Report/ReportContext.tsx
@@ -1,5 +1,6 @@
+import { DailyPoolState } from '@centrifuge/centrifuge-js'
import * as React from 'react'
-import { useParams } from 'react-router'
+import { useLocation, useParams } from 'react-router'
import { useSearchParams } from 'react-router-dom'
export type GroupBy = 'day' | 'month' | 'quarter' | 'year' | 'daily'
@@ -49,6 +50,9 @@ export type ReportContextType = {
loan: string
setLoan: (type: string) => void
+
+ reportData: DailyPoolState[]
+ setReportData: (type: DailyPoolState[]) => void
}
export type CsvDataProps = {
@@ -63,9 +67,10 @@ export function ReportContextProvider({ children }: { children: React.ReactNode
// Global filters
const { report: reportParam } = useParams<{ report: Report }>()
+ const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams()
- const report = reportParam || 'balance-sheet'
+ const report = reportParam ? reportParam : location.pathname.includes('reporting') ? 'balance-sheet' : 'investor-tx'
const [startDate, setStartDate] = React.useState('')
const [endDate, setEndDate] = React.useState(new Date().toISOString().slice(0, 10))
@@ -78,6 +83,7 @@ export function ReportContextProvider({ children }: { children: React.ReactNode
const [address, setAddress] = React.useState(searchParams.get('address') || '')
const [network, setNetwork] = React.useState(searchParams.get('network') || 'all')
const [loan, setLoan] = React.useState(searchParams.get('loan') || '')
+ const [reportData, setReportData] = React.useState([])
React.useEffect(() => {
const startDate = searchParams.get('from')
@@ -170,6 +176,8 @@ export function ReportContextProvider({ children }: { children: React.ReactNode
setNetwork: (value: any) => updateParamValues('network', value),
loan,
setLoan: (value: string) => updateParamValues('asset', value),
+ reportData,
+ setReportData,
}}
>
{children}
diff --git a/centrifuge-app/src/components/Report/ReportFilter.tsx b/centrifuge-app/src/components/Report/ReportFilter.tsx
index 72193d6306..e4fcb9c62e 100644
--- a/centrifuge-app/src/components/Report/ReportFilter.tsx
+++ b/centrifuge-app/src/components/Report/ReportFilter.tsx
@@ -1,356 +1,197 @@
-import { Loan, Pool } from '@centrifuge/centrifuge-js'
-import { useGetNetworkName } from '@centrifuge/centrifuge-react'
-import { AnchorButton, Box, DateInput, SearchInput, Select, Shelf } from '@centrifuge/fabric'
+import { CurrencyBalance, DailyPoolState, Pool } from '@centrifuge/centrifuge-js'
+import {
+ AnchorButton,
+ Box,
+ Button,
+ DateInput,
+ IconBalanceSheet,
+ IconCashflow,
+ IconDownload,
+ IconProfitAndLoss,
+ Select,
+ Shelf,
+} from '@centrifuge/fabric'
import * as React from 'react'
import { useNavigate } from 'react-router'
-import { nftMetadataSchema } from '../../schemas'
+import styled from 'styled-components'
+import { usePool, usePoolMetadata } from '../../../src/utils/usePools'
import { useBasePath } from '../../utils/useBasePath'
-import { useActiveDomains } from '../../utils/useLiquidityPools'
-import { useLoans } from '../../utils/useLoans'
-import { useMetadata } from '../../utils/useMetadata'
-import { useCentNFT } from '../../utils/useNFTs'
-import { useDebugFlags } from '../DebugFlags'
-import { GroupBy, Report, ReportContext } from './ReportContext'
-import { formatPoolFeeTransactionType } from './utils'
+import { SimpleBarChart } from '../Charts/SimpleBarChart'
+import { GroupBy, ReportContext } from './ReportContext'
+
+interface StyledButtonProps {
+ selected?: boolean
+}
+
+const StyledButton = styled(Button)`
+ margin-bottom: 12px;
+ margin-right: 12px;
+ @media (min-width: ${({ theme }) => theme.breakpoints['M']}) {
+ margin-bottom: 0;
+ }
+ & > span {
+ border-color: ${({ selected, theme }) => (selected ? 'transparent' : theme.colors.backgroundInverted)};
+ }
+ &:hover > span {
+ border-color: ${({ selected, theme }) => (selected ? 'transparent' : theme.colors.backgroundInverted)};
+ color: ${({ selected, theme }) => (!selected ? theme.colors.textPrimary : theme.colors.textInverted)};
+ }
+`
type ReportFilterProps = {
- pool: Pool
+ poolId: string
}
-export function ReportFilter({ pool }: ReportFilterProps) {
- const {
- csvData,
- setStartDate,
- startDate,
- endDate,
- setEndDate,
- report,
- loanStatus,
- setLoanStatus,
- txType,
- setTxType,
- groupBy,
- setGroupBy,
- activeTranche,
- setActiveTranche,
- address,
- setAddress,
- network,
- setNetwork,
- loan,
- setLoan,
- } = React.useContext(ReportContext)
+export function ReportFilter({ poolId }: ReportFilterProps) {
+ const { csvData, setStartDate, startDate, endDate, setEndDate, groupBy, setGroupBy, report, reportData } =
+ React.useContext(ReportContext)
const navigate = useNavigate()
const basePath = useBasePath()
+ const pool = usePool(poolId) as Pool
+ const metadata = usePoolMetadata(pool as Pool)
- const { data: domains } = useActiveDomains(pool.id)
- const getNetworkName = useGetNetworkName()
- const loans = useLoans(pool.id) as Loan[] | undefined
-
- const { showOracleTx } = useDebugFlags()
-
- const reportOptions: { label: string; value: Report }[] = [
- { label: 'Balance sheet', value: 'balance-sheet' },
- { label: 'Profit & loss', value: 'profit-and-loss' },
- { label: 'Cash flow statement', value: 'cash-flow-statement' },
- { label: 'Investor transactions', value: 'investor-tx' },
- { label: 'Asset transactions', value: 'asset-tx' },
- { label: 'Fee transactions', value: 'fee-tx' },
- ...(showOracleTx === true ? [{ label: 'Oracle transactions', value: 'oracle-tx' as Report }] : []),
- // { label: 'Pool balance', value: 'pool-balance' },
- { label: 'Token price', value: 'token-price' },
- { label: 'Asset list', value: 'asset-list' },
- { label: 'Investor list', value: 'investor-list' },
- ]
+ const transformDataChart = React.useMemo(() => {
+ if (!reportData.length) return
+ if (report === 'balance-sheet') {
+ return reportData.map((data: DailyPoolState) => ({
+ name: data.timestamp,
+ yAxis: new CurrencyBalance(data.poolState.netAssetValue, pool.currency.decimals).toNumber(),
+ }))
+ } else if (report === 'profit-and-loss') {
+ return reportData.map((data: DailyPoolState) => {
+ return {
+ name: data.timestamp,
+ yAxis: (metadata?.data?.pool?.asset.class === 'Private credit'
+ ? data.poolState.sumInterestRepaidAmountByPeriod
+ .add(data.poolState.sumInterestAccruedByPeriod)
+ .add(data.poolState.sumUnscheduledRepaidAmountByPeriod)
+ .sub(data.poolState.sumDebtWrittenOffByPeriod)
+ : data.poolState.sumUnrealizedProfitByPeriod
+ .add(data.poolState.sumInterestRepaidAmountByPeriod)
+ .add(data.poolState.sumUnscheduledRepaidAmountByPeriod)
+ )
+ .sub(data.poolState.sumPoolFeesChargedAmountByPeriod)
+ .sub(data.poolState.sumPoolFeesAccruedAmountByPeriod)
+ .toNumber(),
+ }
+ })
+ } else {
+ return reportData.map((data: DailyPoolState) => {
+ return {
+ name: data.timestamp,
+ yAxis: data.poolState.sumPrincipalRepaidAmountByPeriod
+ .sub(data.poolState.sumBorrowedAmountByPeriod)
+ .add(data.poolState.sumInterestRepaidAmountByPeriod)
+ .add(data.poolState.sumUnscheduledRepaidAmountByPeriod)
+ .sub(data.poolState.sumPoolFeesPaidAmountByPeriod)
+ .add(data.poolState.sumInvestedAmountByPeriod)
+ .sub(data.poolState.sumRedeemedAmountByPeriod)
+ .toNumber(),
+ }
+ })
+ }
+ }, [report, reportData, metadata?.data?.pool?.asset.class, pool.currency.decimals])
return (
-
)
}
-
-function LoanOption({ loan }: { loan: Loan }) {
- const nft = useCentNFT(loan.asset.collectionId, loan.asset.nftId, false, false)
- const { data: metadata } = useMetadata(nft?.metadataUri, nftMetadataSchema)
- return (
-
- )
-}
diff --git a/centrifuge-app/src/components/Report/index.tsx b/centrifuge-app/src/components/Report/index.tsx
index 3836463b05..2cb980da04 100644
--- a/centrifuge-app/src/components/Report/index.tsx
+++ b/centrifuge-app/src/components/Report/index.tsx
@@ -1,8 +1,7 @@
import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
-import { Box, Shelf, Text } from '@centrifuge/fabric'
+import { Box } from '@centrifuge/fabric'
import Decimal from 'decimal.js-light'
import * as React from 'react'
-import { formatDate } from '../../utils/date'
import { AssetList } from './AssetList'
import { AssetTransactions } from './AssetTransactions'
import { BalanceSheet } from './BalanceSheet'
@@ -23,26 +22,10 @@ export type TableDataRow = {
}
export function ReportComponent({ pool }: { pool: Pool }) {
- const { report, startDate, endDate } = React.useContext(ReportContext)
+ const { report } = React.useContext(ReportContext)
return (
-
-
- {!['investor-list', 'asset-list'].includes(report) && (
- <>
- {startDate ? formatDate(startDate) : 'The beginning of time'}
- {' - '}
- {endDate ? formatDate(endDate) : 'now'}
- >
- )}
-
- {['pool-balance', 'asset-list'].includes(report) && pool && (
-
- All amounts are in {pool.currency.symbol}
-
- )}
-
{report === 'pool-balance' && }
{report === 'token-price' && }
diff --git a/centrifuge-app/src/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx
index 96ced10a00..f6a3f78110 100644
--- a/centrifuge-app/src/components/Tooltips.tsx
+++ b/centrifuge-app/src/components/Tooltips.tsx
@@ -338,22 +338,30 @@ export const tooltipText = {
label: 'Target APY',
body: 'The target APY for the tranche.',
},
+ expenseRatio: {
+ label: 'Expense Ratio',
+ body: 'The operating expenses of the fund as a percentage of the total NAV',
+ },
}
export type TooltipsProps = {
type?: keyof typeof tooltipText
- variant?: 'primary' | 'secondary'
label?: string | React.ReactNode
props?: any
+ size?: 'med' | 'sm'
} & Partial>
-export function Tooltips({ type, label: labelOverride, variant = 'primary', props, ...textProps }: TooltipsProps) {
+export function Tooltips({ type, label: labelOverride, size = 'sm', props, ...textProps }: TooltipsProps) {
const { label, body } = type ? tooltipText[type] : { label: labelOverride, body: textProps.body }
- const isPrimary = variant === 'primary'
return (
{typeof label === 'string' ? (
-
+
{labelOverride || label}
) : (
diff --git a/centrifuge-app/src/pages/IssuerCreatePool/CustomCategories.tsx b/centrifuge-app/src/pages/IssuerCreatePool/CustomCategories.tsx
new file mode 100644
index 0000000000..745354880e
--- /dev/null
+++ b/centrifuge-app/src/pages/IssuerCreatePool/CustomCategories.tsx
@@ -0,0 +1,128 @@
+import { PoolMetadataInput } from '@centrifuge/centrifuge-js/dist/modules/pools'
+import { Box, Button, NumberInput, Select, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric'
+import { Field, FieldArray, FieldProps, useFormikContext } from 'formik'
+
+const OPTIONS = [
+ { label: 'Fund admin', value: 'fundAdmin' },
+ { label: 'Trustee', value: 'trustee' },
+ { label: 'Pricing oracle provider', value: 'pricingOracleProvider' },
+ { label: 'Auditor', value: 'auditor' },
+ { label: 'Custodian', value: 'custodian' },
+ { label: 'Investment manager', value: 'Investment manager' },
+ { label: 'Sub-advisor', value: 'subadvisor' },
+ { label: 'Historical default rate', value: 'historicalDefaultRate' },
+ { label: 'Other', value: 'other' },
+]
+
+const createCategory = () => ({
+ type: 'fundAdmin',
+ value: '',
+})
+
+export type IssuerDetail = {
+ type: string
+ value: string
+ customType?: string
+}
+
+export function CustomCategories() {
+ const fmk = useFormikContext()
+ const { values } = fmk
+
+ return (
+
+ {(fldArr) => (
+
+
+
+ Issuer profile categories
+
+ Add additional information
+
+
+ {
+ fldArr.push(createCategory())
+ }}
+ small
+ >
+ {values.issuerCategories?.length ? 'Add another' : 'Add'}
+
+
+
+ {!!values?.issuerCategories?.length &&
+ values.issuerCategories.map((category, index) => {
+ return (
+
+ {({ field, meta }: FieldProps) => {
+ return (
+
+
+ {
+ const selectedValue = event.target.value
+ fmk.setFieldValue(`issuerCategories.${index}.type`, selectedValue)
+ if (selectedValue !== 'other') {
+ fmk.setFieldValue(`issuerCategories.${index}.customType`, '')
+ }
+ }}
+ onBlur={field.onBlur}
+ errorMessage={meta.touched && meta.error ? meta.error : undefined}
+ value={field.value}
+ options={OPTIONS}
+ label="Type"
+ />
+
+ {category.type === 'other' && (
+
+ ) => {
+ fmk.setFieldValue(`issuerCategories.${index}.customType`, event.target.value)
+ }}
+ />
+
+ )}
+
+ ) => {
+ fmk.setFieldValue(`issuerCategories.${index}.value`, event.target.value)
+ }}
+ onBlur={field.onBlur}
+ value={category.value}
+ label="Value"
+ />
+
+
+ {
+ fldArr.remove(index)
+ }}
+ >
+ Remove
+
+
+
+ )
+ }}
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx b/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx
index 456a24e85e..69fbb682e1 100644
--- a/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx
+++ b/centrifuge-app/src/pages/IssuerCreatePool/IssuerInput.tsx
@@ -3,6 +3,7 @@ import { Field, FieldProps } from 'formik'
import { FieldWithErrorMessage } from '../../components/FieldWithErrorMessage'
import { Tooltips } from '../../components/Tooltips'
import { isTestEnv } from '../../config'
+import { CustomCategories } from './CustomCategories'
import { CustomDetails } from './CustomDetails'
import { validate } from './validate'
@@ -132,6 +133,9 @@ export function IssuerInput({ waitingForStoredIssuer = false }: Props) {
+
+
+
)
}
diff --git a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx
index 1d896377eb..33eddcef88 100644
--- a/centrifuge-app/src/pages/IssuerCreatePool/index.tsx
+++ b/centrifuge-app/src/pages/IssuerCreatePool/index.tsx
@@ -114,6 +114,7 @@ export type CreatePoolValues = Omit<
poolType: 'open' | 'closed'
investorType: string
issuerShortDescription: string
+ issuerCategories: { type: string; value: string }[]
ratingAgency: string
ratingValue: string
ratingReportUrl: string
@@ -137,6 +138,7 @@ const initialValues: CreatePoolValues = {
issuerLogo: null,
issuerDescription: '',
issuerShortDescription: '',
+ issuerCategories: [],
executiveSummary: null,
website: '',
diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx
index 5f04058d27..e7edcf09d0 100644
--- a/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx
+++ b/centrifuge-app/src/pages/IssuerPool/Configuration/Issuer.tsx
@@ -6,7 +6,7 @@ import * as React from 'react'
import { useParams } from 'react-router'
import { lastValueFrom } from 'rxjs'
import { ButtonGroup } from '../../../components/ButtonGroup'
-import { IssuerDetails, RatingDetails, ReportDetails } from '../../../components/IssuerSection'
+import { IssuerDetails, PoolAnalysis, RatingDetails } from '../../../components/IssuerSection'
import { PageSection } from '../../../components/PageSection'
import { getFileDataURI } from '../../../utils/getFileDataURI'
import { useFile } from '../../../utils/useFile'
@@ -25,6 +25,7 @@ type Values = Pick<
| 'issuerLogo'
| 'issuerDescription'
| 'issuerShortDescription'
+ | 'issuerCategories'
| 'executiveSummary'
| 'website'
| 'forum'
@@ -60,6 +61,7 @@ export function Issuer() {
issuerLogo: logoFile ?? null,
issuerDescription: metadata?.pool?.issuer?.description ?? '',
issuerShortDescription: metadata?.pool?.issuer?.shortDescription ?? '',
+ issuerCategories: metadata?.pool?.issuer?.categories ?? [],
executiveSummary: metadata?.pool?.links?.executiveSummary ? 'executiveSummary.pdf' : ('' as any),
website: metadata?.pool?.links?.website ?? '',
forum: metadata?.pool?.links?.forum ?? '',
@@ -120,6 +122,7 @@ export function Issuer() {
logo:
logoChanged && logoUri ? { uri: logoUri, mime: values.issuerLogo!.type } : oldMetadata.pool.issuer.logo,
shortDescription: values.issuerShortDescription,
+ categories: values.issuerCategories,
},
links: {
executiveSummary: execSummaryUri
@@ -216,7 +219,7 @@ export function Issuer() {
) : (
- {metadata?.pool?.reports?.[0] && }
+ {metadata?.pool?.reports?.[0] && }
{metadata?.pool?.rating && }
)}
diff --git a/centrifuge-app/src/pages/IssuerPool/Header.tsx b/centrifuge-app/src/pages/IssuerPool/Header.tsx
index e484bf85bb..334d64e2ec 100644
--- a/centrifuge-app/src/pages/IssuerPool/Header.tsx
+++ b/centrifuge-app/src/pages/IssuerPool/Header.tsx
@@ -77,7 +77,8 @@ export function IssuerPoolHeader({ actions }: Props) {
Overview
Assets
Liquidity
- {!isTinlakePool && Reporting}
+ {!isTinlakePool && Reports}
+ {!isTinlakePool && Data}
Investors
Configuration
Access
diff --git a/centrifuge-app/src/pages/IssuerPool/index.tsx b/centrifuge-app/src/pages/IssuerPool/index.tsx
index e08846e39f..8bfe1a8c47 100644
--- a/centrifuge-app/src/pages/IssuerPool/index.tsx
+++ b/centrifuge-app/src/pages/IssuerPool/index.tsx
@@ -32,6 +32,8 @@ export default function IssuerPoolPage() {
} />
} />
} />
+ } />
+ } />
} />
} />
diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
index 792f501e4a..0cea78c852 100644
--- a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
+++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx
@@ -218,7 +218,7 @@ export function NavManagementAssetTable({ poolId }: { poolId: string }) {
pool.currency.decimals
)
}, new CurrencyBalance(0, pool.currency.decimals))
- }, [externalLoans, pool?.nav, form.values.feed])
+ }, [externalLoans, form.values.feed, pool.currency.decimals])
const pendingNav = totalAum.add(changeInValuation.toDecimal()).sub(pendingFees.toDecimal())
diff --git a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
index cc65cacbbb..3fa2277083 100644
--- a/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
+++ b/centrifuge-app/src/pages/Onboarding/CompleteExternalOnboarding.tsx
@@ -10,7 +10,7 @@ type Props = {
}
export const CompleteExternalOnboarding = ({ openNewTab, poolId, poolSymbol }: Props) => {
- const { refetchOnboardingUser, isOnboardingExternally } = useOnboarding()
+ const { refetchOnboardingUser } = useOnboarding()
const onFocus = () => {
refetchOnboardingUser()
diff --git a/centrifuge-app/src/pages/Pool/Header.tsx b/centrifuge-app/src/pages/Pool/Header.tsx
index 8c6eba054d..c9d1a7e996 100644
--- a/centrifuge-app/src/pages/Pool/Header.tsx
+++ b/centrifuge-app/src/pages/Pool/Header.tsx
@@ -3,7 +3,6 @@ import { Box, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric'
import * as React from 'react'
import { useLocation, useParams } from 'react-router'
import { useTheme } from 'styled-components'
-import { Eththumbnail } from '../../components/EthThumbnail'
import { BASE_PADDING } from '../../components/LayoutBase/BasePadding'
import { NavigationTabs, NavigationTabsItem } from '../../components/NavigationTabs'
import { PageHeader } from '../../components/PageHeader'
@@ -30,14 +29,11 @@ export function PoolDetailHeader({ actions }: Props) {
return (
{metadata?.pool?.name ?? 'Unnamed pool'}}
- subtitle={
- by {metadata?.pool?.issuer.name ?? 'Unknown'}
- }
parent={{ to: `/pools${state?.token ? '/tokens' : ''}`, label: state?.token ? 'Tokens' : 'Pools' }}
icon={
-
+ <>
{metadata?.pool?.icon ? (
-
+
) : (
{(isLoading ? '' : metadata?.pool?.name ?? 'U')[0]}
)}
-
+ >
}
border={false}
actions={actions}
@@ -67,7 +63,8 @@ export function PoolDetailHeader({ actions }: Props) {
Overview
Assets
Liquidity
- {!isTinlakePool && Reporting}
+ {!isTinlakePool && Reports}
+ {!isTinlakePool && Data}
{!isTinlakePool && Fees}
diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx
index 9ab15172a9..6a7b3fb4bd 100644
--- a/centrifuge-app/src/pages/Pool/Overview/index.tsx
+++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx
@@ -1,5 +1,5 @@
-import { CurrencyBalance, Price, Rate } from '@centrifuge/centrifuge-js'
-import { Box, Button, Card, Grid, IconFileText, Stack, Text, TextWithPlaceholder } from '@centrifuge/fabric'
+import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js'
+import { Box, Button, Card, Grid, TextWithPlaceholder } from '@centrifuge/fabric'
import Decimal from 'decimal.js-light'
import * as React from 'react'
import { useParams } from 'react-router'
@@ -9,10 +9,8 @@ import { InvestRedeemDrawer } from '../../../components/InvestRedeem/InvestRedee
import { IssuerDetails, ReportDetails } from '../../../components/IssuerSection'
import { LayoutSection } from '../../../components/LayoutBase/LayoutSection'
import { LoadBoundary } from '../../../components/LoadBoundary'
-import { Cashflows } from '../../../components/PoolOverview/Cashflows'
import { KeyMetrics } from '../../../components/PoolOverview/KeyMetrics'
import { PoolPerformance } from '../../../components/PoolOverview/PoolPerfomance'
-import { PoolStructure } from '../../../components/PoolOverview/PoolStructure'
import { TrancheTokenCards } from '../../../components/PoolOverview/TrancheTokenCards'
import { TransactionHistory } from '../../../components/PoolOverview/TransactionHistory'
import { Spinner } from '../../../components/Spinner'
@@ -23,8 +21,7 @@ import { getPoolValueLocked } from '../../../utils/getPoolValueLocked'
import { useAverageMaturity } from '../../../utils/useAverageMaturity'
import { useConnectBeforeAction } from '../../../utils/useConnectBeforeAction'
import { useIsAboveBreakpoint } from '../../../utils/useIsAboveBreakpoint'
-import { useLoans } from '../../../utils/useLoans'
-import { usePool, usePoolFees, usePoolMetadata } from '../../../utils/usePools'
+import { usePool, usePoolMetadata } from '../../../utils/usePools'
import { PoolDetailHeader } from '../Header'
export type Token = {
@@ -71,11 +68,7 @@ export function PoolDetailOverview() {
const isTinlakePool = poolId.startsWith('0x')
const pool = usePool(poolId)
- const poolFees = usePoolFees(poolId)
const { data: metadata, isLoading: metadataIsLoading } = usePoolMetadata(pool)
- const averageMaturity = useAverageMaturity(poolId)
- const loans = useLoans(poolId)
- const isMedium = useIsAboveBreakpoint('M')
const pageSummaryData = [
{
@@ -109,100 +102,43 @@ export function PoolDetailOverview() {
id: tranche.id,
capacity: tranche.capacity,
tokenPrice: tranche.tokenPrice,
- yield30DaysAnnualized: tranche?.yield30DaysAnnualized,
+ yield30DaysAnnualized: tranche?.yield30DaysAnnualized?.toString() || '',
}
})
.reverse()
return (
-
-
-
- }>
-
-
-
- {tokens.length > 0 && (
+
+
+
+
+ }>
+
+
+
+ {tokens.length > 0 && (
+ }>
+
+
+ )}
}>
-
-
- )}
- }>
- {metadata?.pool?.reports?.length || !isTinlakePool ? (
-
-
-
-
-
-
- Reports
-
-
-
-
-
- Issuer details
-
-
-
-
- ) : null}
- {isTinlakePool && (
-
-
- Issuer details
+
+
-
-
- )}
-
- {!isTinlakePool && (
- <>
-
- }>
- {
- return {
- fee: poolFees?.find((f) => f.id === fee.id)?.amounts.percentOfNav ?? Rate.fromFloat(0),
- name: fee.name,
- id: fee.id,
- }
- }) || []
- }
- />
-
- {/* }>
-
- */}
-
- {isMedium && (
- }>
-
-
+
+ {metadata?.pool?.reports?.length || !isTinlakePool ? (
+
+
-
- )}
+ ) : null}
+
+
+ {!isTinlakePool && (
}>
-
-
-
+
- >
- )}
+ )}
+
)
}
diff --git a/centrifuge-app/src/pages/Pool/index.tsx b/centrifuge-app/src/pages/Pool/index.tsx
index ff51232aac..ffc8233867 100644
--- a/centrifuge-app/src/pages/Pool/index.tsx
+++ b/centrifuge-app/src/pages/Pool/index.tsx
@@ -10,7 +10,9 @@ export default function PoolDetailPage() {
} />
} />
+ } />
} />
+ } />
} />
} />
} />
diff --git a/centrifuge-app/src/pages/Pools.tsx b/centrifuge-app/src/pages/Pools.tsx
index cd7f41d52f..08fd0188b3 100644
--- a/centrifuge-app/src/pages/Pools.tsx
+++ b/centrifuge-app/src/pages/Pools.tsx
@@ -1,7 +1,7 @@
import { formatBalance } from '@centrifuge/centrifuge-react'
-import { Box, IconArrowDown, IconArrowUpRight, Stack, StatusChip, Text } from '@centrifuge/fabric'
+import { Box, Stack, Text } from '@centrifuge/fabric'
import * as React from 'react'
-import { useListedPools, useYearOverYearGrowth } from '../../src/utils/useListedPools'
+import { useListedPools } from '../../src/utils/useListedPools'
import { LayoutSection } from '../components/LayoutBase/LayoutSection'
import { PoolList } from '../components/PoolList'
import { prefetchRoute } from '../components/Root'
@@ -10,9 +10,6 @@ import { Dec } from '../utils/Decimal'
export default function PoolsPage() {
const [, listedTokens] = useListedPools()
- const { totalYoyGrowth, isLoading } = useYearOverYearGrowth()
- const isPositiveYoy = totalYoyGrowth > 0
- const IconComponent = isPositiveYoy ? IconArrowUpRight : IconArrowDown
const totalValueLocked = React.useMemo(() => {
return (
@@ -44,16 +41,6 @@ export default function PoolsPage() {
Total value locked (TVL)
- {!isLoading && (
-
-
-
-
- {formatBalance(totalYoyGrowth ?? 0, '', 2)} YoY
-
-
-
- )}
{formatBalance(totalValueLocked ?? 0, config.baseCurrency)}
diff --git a/centrifuge-app/src/utils/formatting.ts b/centrifuge-app/src/utils/formatting.ts
index 4db8c2d82a..b301b0e91c 100644
--- a/centrifuge-app/src/utils/formatting.ts
+++ b/centrifuge-app/src/utils/formatting.ts
@@ -35,15 +35,18 @@ export function formatBalanceAbbreviated(
? amount.toNumber()
: amount
let formattedAmount = ''
- if (amountNumber >= 1e9) {
+ const absAmount = Math.abs(amountNumber)
+
+ if (absAmount >= 1e9) {
formattedAmount = `${(amountNumber / 1e9).toFixed(decimals)}B`
- } else if (amountNumber >= 1e6) {
+ } else if (absAmount >= 1e6) {
formattedAmount = `${(amountNumber / 1e6).toFixed(decimals)}M`
- } else if (amountNumber > 999) {
+ } else if (absAmount > 999) {
formattedAmount = `${(amountNumber / 1e3).toFixed(decimals)}K`
} else {
formattedAmount = `${amountNumber.toFixed(decimals)}`
}
+
return currency ? `${formattedAmount} ${currency}` : formattedAmount
}
diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts
index 2a07f549c4..9dd6e73b51 100644
--- a/centrifuge-js/src/modules/pools.ts
+++ b/centrifuge-js/src/modules/pools.ts
@@ -684,6 +684,7 @@ export interface PoolMetadataInput {
issuerLogo?: FileType | null
issuerDescription: string
issuerShortDescription: string
+ issuerCategories: { type: string; value: string; customType?: string }[]
poolReport?: {
authorName: string
@@ -748,6 +749,7 @@ export type PoolMetadata = {
email: string
logo?: FileType | null
shortDescription: string
+ categories: { type: string; value: string; customType?: string }[]
}
links: {
executiveSummary: FileType | null
@@ -1129,6 +1131,7 @@ export function getPoolsModule(inst: Centrifuge) {
email: metadata.email,
logo: metadata.issuerLogo,
shortDescription: metadata.issuerShortDescription,
+ categories: metadata.issuerCategories,
},
poolStructure: metadata.poolStructure,
investorType: metadata.investorType,
@@ -2469,13 +2472,19 @@ export function getPoolsModule(inst: Centrifuge) {
}),
takeLast(1),
map(({ trancheSnapshots }) => {
- const trancheStates: Record = {}
+ const trancheStates: Record<
+ string,
+ { timestamp: string; tokenPrice: Price; yield30DaysAnnualized: Perquintill }[]
+ > = {}
trancheSnapshots?.forEach((state) => {
const tid = state.tranche.trancheId
const entry = {
timestamp: state.timestamp,
tokenPrice: new Price(state.tokenPrice),
pool: state.tranche.poolId,
+ yield30DaysAnnualized: state.yield30DaysAnnualized
+ ? new Perquintill(state.yield30DaysAnnualized)
+ : new Perquintill(0),
}
if (trancheStates[tid]) {
trancheStates[tid].push(entry)
@@ -2665,26 +2674,25 @@ export function getPoolsModule(inst: Centrifuge) {
poolCurrency.decimals
),
yield7DaysAnnualized: tranche.yield7DaysAnnualized
- ? new Perquintill(hexToBN(tranche.yield7DaysAnnualized))
+ ? new Perquintill(tranche.yield7DaysAnnualized)
: new Perquintill(0),
yield30DaysAnnualized: tranche.yield30DaysAnnualized
- ? new Perquintill(hexToBN(tranche.yield30DaysAnnualized))
+ ? new Perquintill(tranche.yield30DaysAnnualized)
: new Perquintill(0),
yield90DaysAnnualized: tranche.yield90DaysAnnualized
- ? new Perquintill(hexToBN(tranche.yield90DaysAnnualized))
+ ? new Perquintill(tranche.yield90DaysAnnualized)
: new Perquintill(0),
yieldSinceInception: tranche.yieldSinceInception
- ? new Perquintill(hexToBN(tranche.yieldSinceInception))
+ ? new Perquintill(tranche.yieldSinceInception)
: new Perquintill(0),
- yieldMTD: tranche.yieldMTD ? new Perquintill(hexToBN(tranche.yieldMTD)) : new Perquintill(0),
- yieldQTD: tranche.yieldQTD ? new Perquintill(hexToBN(tranche.yieldQTD)) : new Perquintill(0),
- yieldYTD: tranche.yieldYTD ? new Perquintill(hexToBN(tranche.yieldYTD)) : new Perquintill(0),
+ yieldMTD: tranche.yieldMTD ? new Perquintill(tranche.yieldMTD) : new Perquintill(0),
+ yieldQTD: tranche.yieldQTD ? new Perquintill(tranche.yieldQTD) : new Perquintill(0),
+ yieldYTD: tranche.yieldYTD ? new Perquintill(tranche.yieldYTD) : new Perquintill(0),
yieldSinceLastPeriod: tranche.yieldSinceLastPeriod
- ? new Perquintill(hexToBN(tranche.yieldSinceLastPeriod))
+ ? new Perquintill(tranche.yieldSinceLastPeriod)
: new Perquintill(0),
}
})
-
return { ...state, poolState, poolValue, tranches }
}) || [],
trancheStates,
@@ -4629,7 +4637,7 @@ export function getPoolsModule(inst: Centrifuge) {
}
}
-function hexToBN(value?: string | number | null) {
+export function hexToBN(value?: string | number | null) {
if (typeof value === 'number' || value == null) return new BN(value ?? 0)
return new BN(value.toString().substring(2), 'hex')
}
diff --git a/fabric/.storybook/preview.jsx b/fabric/.storybook/preview.jsx
index 24da97cb2a..3bfd399ddc 100644
--- a/fabric/.storybook/preview.jsx
+++ b/fabric/.storybook/preview.jsx
@@ -1,13 +1,9 @@
import * as React from 'react'
import { ThemeProvider } from 'styled-components'
import { Box, GlobalStyle } from '../src'
-import altairDark from '../src/theme/altairDark'
-import altairLight from '../src/theme/altairLight'
import centrifugeTheme from '../src/theme/centrifugeTheme'
const themes = {
- altairDark,
- altairLight,
centrifugeTheme,
}
diff --git a/fabric/src/components/Button/WalletButton.tsx b/fabric/src/components/Button/WalletButton.tsx
index e89aab6536..47a32bd635 100644
--- a/fabric/src/components/Button/WalletButton.tsx
+++ b/fabric/src/components/Button/WalletButton.tsx
@@ -30,6 +30,12 @@ const StyledButton = styled.button`
outline: 0;
border-radius: 40px;
white-space: nowrap;
+ & > span {
+ border-color: ${({ theme }) => theme.colors.backgroundPrimary};
+ :hover {
+ border-color: ${({ theme }) => theme.colors.backgroundPrimary};
+ }
+ }
`
const IdenticonWrapper = styled(Flex)({
diff --git a/fabric/src/components/InputUnit/index.tsx b/fabric/src/components/InputUnit/index.tsx
index 0de1fd4e19..831969820d 100644
--- a/fabric/src/components/InputUnit/index.tsx
+++ b/fabric/src/components/InputUnit/index.tsx
@@ -15,15 +15,21 @@ export type InputUnitProps = {
errorMessage?: string
inputElement?: React.ReactNode
disabled?: boolean
+ row?: boolean
}
-export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElement, disabled }: InputUnitProps) {
+export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElement, disabled, row }: InputUnitProps) {
const defaultId = React.useId()
id ??= defaultId
+
return (
-
- {label && {label}}
+
+ {label && (
+
+ {label}
+
+ )}
{secondaryLabel && (
-
+
{secondaryLabel}
)}
@@ -46,9 +52,22 @@ export function InputUnit({ id, label, secondaryLabel, errorMessage, inputElemen
)
}
-export function InputLabel({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) {
+export function InputLabel({
+ children,
+ disabled,
+ row,
+}: {
+ children: React.ReactNode
+ disabled?: boolean
+ row?: boolean
+}) {
return (
-
+
{children}
)
diff --git a/fabric/src/components/Select/index.tsx b/fabric/src/components/Select/index.tsx
index 38fa43e539..e6e1eb8afa 100644
--- a/fabric/src/components/Select/index.tsx
+++ b/fabric/src/components/Select/index.tsx
@@ -17,6 +17,7 @@ export type SelectProps = React.SelectHTMLAttributes & {
placeholder?: string
errorMessage?: string
small?: boolean
+ hideBorder?: boolean
}
const StyledSelect = styled.select`
@@ -31,14 +32,11 @@ const StyledSelect = styled.select`
cursor: pointer;
line-height: inherit;
text-overflow: ellipsis;
+ font-weight: 500;
&:disabled {
cursor: default;
}
-
- &:focus {
- color: ${({ theme }) => theme.colors.textSelected};
- }
`
export function SelectInner({
@@ -79,7 +77,7 @@ export function SelectInner({
)
}
-export function Select({ label, errorMessage, id, ...rest }: SelectProps) {
+export function Select({ label, errorMessage, id, hideBorder, ...rest }: SelectProps) {
const defaultId = React.useId()
id ??= defaultId
return (
@@ -89,7 +87,7 @@ export function Select({ label, errorMessage, id, ...rest }: SelectProps) {
disabled={rest.disabled}
errorMessage={errorMessage}
inputElement={
-
+
}
diff --git a/fabric/src/components/Tabs/index.tsx b/fabric/src/components/Tabs/index.tsx
index 0e7a40e828..1d5ff62d13 100644
--- a/fabric/src/components/Tabs/index.tsx
+++ b/fabric/src/components/Tabs/index.tsx
@@ -29,7 +29,7 @@ export function Tabs({ selectedIndex, onChange, children }: TabsProps) {
)
}
-const StyledTabsItem = styled.button<{ $active?: boolean }>(
+const StyledTabsItem = styled.button<{ $active?: boolean; styleOverrides?: React.CSSProperties, showBorder?: boolean }>(
{
display: 'flex',
alignItems: 'center',
@@ -43,34 +43,56 @@ const StyledTabsItem = styled.button<{ $active?: boolean }>(
appearance: 'none',
background: 'transparent',
},
- ({ $active, theme }) => {
+ ({ $active, theme, styleOverrides, showBorder }) => {
return css({
paddingTop: 1,
paddingLeft: 2,
paddingRight: 2,
paddingBottom: 2,
- color: 'textPrimary',
- boxShadow: $active ? `inset 0 -2px 0 ${theme.colors.textGold}` : 'none',
+ color: $active ? 'textPrimary' : 'textSecondary',
+ boxShadow: $active ? `inset 0 -2px 0 ${theme.colors.textGold}` : showBorder ? `inset 0 -2px 0 ${theme.colors.textDisabled}` : 'none',
+ fontWeight: 400,
'&:hover, &:active, &:focus-visible': {
color: 'textGold',
},
+ ...styleOverrides,
})
}
)
-export type TabsItemProps = Omit, '$active' | 'ariaLabel'>
-
+export type TabsItemProps = Omit, '$active' | 'ariaLabel'> & {
+ styleOverrides?: React.CSSProperties
+ showBorder?: boolean
+}
type TabsItemPrivateProps = TabsItemProps & {
active?: boolean
onClick?: () => void
ariaLabel?: string
+ styleOverrides?: React.CSSProperties
+ showBorder?: boolean
}
-export function TabsItem({ children, active, onClick, ariaLabel, ...rest }: TabsItemPrivateProps) {
+export function TabsItem({
+ children,
+ active,
+ onClick,
+ ariaLabel,
+ styleOverrides,
+ showBorder,
+ ...rest
+}: TabsItemPrivateProps) {
return (
-
-
+
+
{children}
diff --git a/fabric/src/components/TextInput/index.tsx b/fabric/src/components/TextInput/index.tsx
index e48312d350..9f91c83a5a 100644
--- a/fabric/src/components/TextInput/index.tsx
+++ b/fabric/src/components/TextInput/index.tsx
@@ -11,6 +11,7 @@ export type TextInputProps = React.InputHTMLAttributes &
InputUnitProps & {
action?: React.ReactNode
symbol?: React.ReactNode
+ row?: boolean
}
export type TextAreaInputProps = React.InputHTMLAttributes &
InputUnitProps & {
@@ -49,13 +50,12 @@ export const StyledTextInput = styled.input`
margin: 0;
}
`
-
-export const StyledInputBox = styled(Shelf)`
+export const StyledInputBox = styled(Shelf)<{ hideBorder?: boolean }>`
width: 100%;
position: relative;
background: ${({ theme }) => theme.colors.backgroundPage};
- border: 1px solid ${({ theme }) => theme.colors.borderPrimary};
- border-radius: ${({ theme }) => theme.radii.input}px;
+ border: ${({ hideBorder, theme }) => (hideBorder ? 'none' : `1px solid ${theme.colors.borderPrimary}`)};
+ border-radius: ${({ hideBorder, theme }) => (hideBorder ? 'none' : `${theme.radii.input}px`)};
&::before {
content: '';
@@ -80,7 +80,7 @@ export const StyledInputAction = styled.button`
cursor: pointer;
appearance: none;
border: none;
- background: ${(props) => props.theme.colors.backgroundButtonSecondary};
+ background: ${(props) => props.theme.colors.backgroundButtonInverted};
display: flex;
justify-content: center;
align-items: center;
@@ -103,7 +103,7 @@ export const StyledInputAction = styled.button`
export function InputAction({ children, ...props }: React.ButtonHTMLAttributes) {
return (
-
+
{children}
@@ -114,11 +114,12 @@ export function TextInputBox(
props: Omit & {
error?: boolean
inputRef?: React.Ref
+ row?: boolean
}
) {
- const { error, disabled, action, symbol, inputRef, inputElement, ...inputProps } = props
+ const { error, disabled, action, symbol, inputRef, inputElement, row, ...inputProps } = props
return (
-
+
{inputElement ?? }
{symbol && (
@@ -160,7 +161,7 @@ export function SearchInput({ label, secondaryLabel, disabled, errorMessage, id,
type="search"
disabled={disabled}
error={!!errorMessage}
- symbol={}
+ symbol={}
{...inputProps}
/>
}
@@ -168,9 +169,10 @@ export function SearchInput({ label, secondaryLabel, disabled, errorMessage, id,
)
}
-export function DateInput({ label, secondaryLabel, disabled, errorMessage, id, ...inputProps }: TextInputProps) {
+export function DateInput({ label, secondaryLabel, disabled, errorMessage, id, row, ...inputProps }: TextInputProps) {
const defaultId = React.useId()
id ??= defaultId
+
return (
}
diff --git a/fabric/src/components/Tooltip/index.tsx b/fabric/src/components/Tooltip/index.tsx
index 9eb97c49a5..ae0b10090f 100644
--- a/fabric/src/components/Tooltip/index.tsx
+++ b/fabric/src/components/Tooltip/index.tsx
@@ -57,6 +57,7 @@ const placements: {
}
const Container = styled(Stack)<{ pointer: PlacementAxis }>`
+ background-color: ${({ theme }) => theme.colors.backgroundInverted};
filter: ${({ theme }) => `drop-shadow(${theme.shadows.cardInteractive})`};
&::before {
@@ -65,7 +66,7 @@ const Container = styled(Stack)<{ pointer: PlacementAxis }>`
content: '';
position: absolute;
${({ pointer }) => placements[pointer!]}
- border: ${({ theme }) => `var(--size) solid ${theme.colors.backgroundPrimary}`};
+ border: ${({ theme }) => `var(--size) solid ${theme.colors.backgroundInverted}`};
transform: rotate(-45deg);
}
`
@@ -117,7 +118,9 @@ export function Tooltip({
{title}
)}
- {body}
+
+ {body}
+
)}
/>
diff --git a/fabric/src/icon-svg/Icon-balance-sheet.svg b/fabric/src/icon-svg/Icon-balance-sheet.svg
new file mode 100644
index 0000000000..ec8f772b96
--- /dev/null
+++ b/fabric/src/icon-svg/Icon-balance-sheet.svg
@@ -0,0 +1,3 @@
+
diff --git a/fabric/src/icon-svg/Icon-cashflow.svg b/fabric/src/icon-svg/Icon-cashflow.svg
new file mode 100644
index 0000000000..b254ea2001
--- /dev/null
+++ b/fabric/src/icon-svg/Icon-cashflow.svg
@@ -0,0 +1,3 @@
+
diff --git a/fabric/src/icon-svg/Icon-profit-and-loss.svg b/fabric/src/icon-svg/Icon-profit-and-loss.svg
new file mode 100644
index 0000000000..f544e30502
--- /dev/null
+++ b/fabric/src/icon-svg/Icon-profit-and-loss.svg
@@ -0,0 +1,3 @@
+
diff --git a/fabric/src/icon-svg/IconMoody.svg b/fabric/src/icon-svg/IconMoody.svg
new file mode 100644
index 0000000000..2499a357a6
--- /dev/null
+++ b/fabric/src/icon-svg/IconMoody.svg
@@ -0,0 +1,9 @@
+
diff --git a/fabric/src/icon-svg/IconSp.svg b/fabric/src/icon-svg/IconSp.svg
new file mode 100644
index 0000000000..fd64483382
--- /dev/null
+++ b/fabric/src/icon-svg/IconSp.svg
@@ -0,0 +1,10 @@
+
diff --git a/fabric/src/icon-svg/icon-arrow-right-white.svg b/fabric/src/icon-svg/icon-arrow-right-white.svg
new file mode 100644
index 0000000000..d56f3c1f14
--- /dev/null
+++ b/fabric/src/icon-svg/icon-arrow-right-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/fabric/src/theme/altairDark.ts b/fabric/src/theme/altairDark.ts
deleted file mode 100644
index a5090a6e90..0000000000
--- a/fabric/src/theme/altairDark.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { baseTheme } from './tokens/baseTheme'
-import { brandAltair } from './tokens/brandAltair'
-import { black, blueScale, gold, grayScale, yellowScale } from './tokens/colors'
-import { colorTheme } from './tokens/theme'
-import { FabricTheme } from './types'
-
-export const altairDark: FabricTheme = {
- ...baseTheme,
- scheme: 'dark',
- colors: {
- ...brandAltair,
- ...colorTheme.colors,
- primarySelectedBackground: yellowScale[500],
- secondarySelectedBackground: yellowScale[800],
- focus: yellowScale[500],
- borderFocus: yellowScale[500],
- borderSelected: yellowScale[500],
- textSelected: yellowScale[500],
- textInteractive: yellowScale[500],
- textInteractiveHover: yellowScale[500],
- accentScale: blueScale,
- blueScale,
- yellowScale,
- grayScale,
- backgroundInverted: black,
- textGold: gold,
- },
- shadows: {
- ...baseTheme.shadows,
- cardInteractive: '0 1px 5px rgba(255, 255, 255, .8)',
- cardActive: '0 0 0 1px var(--fabric-focus), 0 1px 5px rgba(255, 255, 255, .8)',
- cardOverlay: '4px 8px 24px rgba(255, 255, 255, .4)',
- },
-}
-
-export default altairDark
diff --git a/fabric/src/theme/altairLight.ts b/fabric/src/theme/altairLight.ts
deleted file mode 100644
index 6493140241..0000000000
--- a/fabric/src/theme/altairLight.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { baseTheme } from './tokens/baseTheme'
-import { brandAltair } from './tokens/brandAltair'
-import { blueScale, grayScale, yellowScale } from './tokens/colors'
-import { colorTheme } from './tokens/theme'
-import { FabricTheme } from './types'
-
-export const altairLight: FabricTheme = {
- ...baseTheme,
- scheme: 'light',
- colors: {
- ...brandAltair,
- ...colorTheme.colors,
- primarySelectedBackground: blueScale[500],
- secondarySelectedBackground: blueScale[50],
- focus: blueScale[500],
- borderFocus: blueScale[500],
- borderSelected: blueScale[500],
- textSelected: blueScale[500],
- textInteractive: blueScale[500],
- textInteractiveHover: blueScale[500],
- accentScale: blueScale,
- blueScale,
- yellowScale,
- grayScale,
- },
-}
-
-export default altairLight
diff --git a/fabric/src/theme/centrifugeTheme.ts b/fabric/src/theme/centrifugeTheme.ts
index faac679481..932b205f80 100644
--- a/fabric/src/theme/centrifugeTheme.ts
+++ b/fabric/src/theme/centrifugeTheme.ts
@@ -10,15 +10,15 @@ export const centrifugeTheme: FabricTheme = {
colors: {
...brandCentrifuge,
...colorTheme.colors,
- primarySelectedBackground: blueScale[500],
- secondarySelectedBackground: blueScale[50],
- focus: blueScale[500],
- borderFocus: blueScale[500],
- borderSelected: blueScale[500],
- textSelected: blueScale[500],
- textInteractive: blueScale[500],
- textInteractiveHover: blueScale[500],
- accentScale: blueScale,
+ primarySelectedBackground: yellowScale[500],
+ secondarySelectedBackground: yellowScale[50],
+ focus: grayScale[600],
+ borderFocus: grayScale[500],
+ borderSelected: grayScale[500],
+ textSelected: grayScale[500],
+ textInteractive: grayScale[500],
+ textInteractiveHover: grayScale[500],
+ accentScale: yellowScale,
blueScale,
yellowScale,
grayScale,
diff --git a/fabric/src/theme/index.ts b/fabric/src/theme/index.ts
index 7e00d5fcfe..65cb46c4ed 100644
--- a/fabric/src/theme/index.ts
+++ b/fabric/src/theme/index.ts
@@ -1,4 +1,2 @@
-export * from './altairDark'
-export * from './altairLight'
export * from './centrifugeTheme'
export * from './types'
diff --git a/fabric/src/theme/tokens/baseTheme.ts b/fabric/src/theme/tokens/baseTheme.ts
index 3144752ddd..9853222081 100644
--- a/fabric/src/theme/tokens/baseTheme.ts
+++ b/fabric/src/theme/tokens/baseTheme.ts
@@ -20,7 +20,7 @@ export const baseTheme: Omit = {
radii: {
tooltip: 4,
card: 8,
- input: 2,
+ input: 8,
button: 4,
chip: 4,
},
diff --git a/fabric/src/theme/tokens/colors.ts b/fabric/src/theme/tokens/colors.ts
index a6bc15e96f..2a6386261b 100644
--- a/fabric/src/theme/tokens/colors.ts
+++ b/fabric/src/theme/tokens/colors.ts
@@ -6,6 +6,7 @@ export const grayScale = {
100: '#E7E7E7',
300: '#CFCFCF',
500: '#91969B',
+ 600: '#667085',
800: '#252B34',
900: '#0F1115',
}
diff --git a/fabric/src/theme/tokens/theme.ts b/fabric/src/theme/tokens/theme.ts
index 87824b8bee..c24d2aa394 100644
--- a/fabric/src/theme/tokens/theme.ts
+++ b/fabric/src/theme/tokens/theme.ts
@@ -1,4 +1,4 @@
-import { black, blackScale, blueScale, centrifugeBlue, gold, grayScale, yellowScale } from './colors'
+import { black, blueScale, gold, grayScale, yellowScale } from './colors'
const statusDefault = grayScale[800]
const statusInfo = blueScale[500]
@@ -65,32 +65,32 @@ const colors = {
shadowButtonPrimary: 'transparent',
backgroundButtonSecondary: black,
- backgroundButtonSecondaryFocus: blackScale[500],
- backgroundButtonSecondaryHover: blackScale[500],
- backgroundButtonSecondaryPressed: blackScale[500],
+ backgroundButtonSecondaryFocus: black,
+ backgroundButtonSecondaryHover: black,
+ backgroundButtonSecondaryPressed: black,
backgroundButtonSecondaryDisabled: grayScale[300],
textButtonSecondary: 'white',
- textButtonSecondaryFocus: gold,
- textButtonSecondaryHover: gold,
- textButtonSecondaryPressed: gold,
+ textButtonSecondaryFocus: 'white',
+ textButtonSecondaryHover: 'white',
+ textButtonSecondaryPressed: 'white',
textButtonSecondaryDisabled: grayScale[500],
- borderButtonSecondary: grayScale[300],
- borderButtonSecondaryFocus: gold,
- borderButtonSecondaryHover: gold,
- borderButtonSecondaryPressed: gold,
+ borderButtonSecondary: black,
+ borderButtonSecondaryFocus: black,
+ borderButtonSecondaryHover: black,
+ borderButtonSecondaryPressed: black,
borderButtonSecondaryDisabled: 'transparent',
- shadowButtonSecondary: '#A8BFFD35',
+ shadowButtonSecondary: 'transparent',
backgroundButtonTertiary: 'transparent',
backgroundButtonTertiaryFocus: 'transparent',
backgroundButtonTertiaryHover: 'tranparent',
backgroundButtonTertiaryPressed: 'transparent',
backgroundButtonTertiaryDisabled: 'transparent',
- textButtonTertiary: centrifugeBlue,
- textButtonTertiaryFocus: centrifugeBlue,
- textButtonTertiaryHover: grayScale[800],
- textButtonTertiaryPressed: centrifugeBlue,
- textButtonTertiaryDisabled: grayScale[500],
+ textButtonTertiary: grayScale[800],
+ textButtonTertiaryFocus: gold,
+ textButtonTertiaryHover: gold,
+ textButtonTertiaryPressed: gold,
+ textButtonTertiaryDisabled: grayScale[300],
borderButtonTertiary: 'transparent',
borderButtonTertiaryFocus: 'transparent',
borderButtonTertiaryHover: 'transparent',
@@ -102,17 +102,17 @@ const colors = {
backgroundButtonInvertedHover: grayScale[100],
backgroundButtonInvertedPressed: grayScale[100],
backgroundButtonInvertedDisabled: grayScale[100],
- textButtonInverted: centrifugeBlue,
- textButtonInvertedFocus: centrifugeBlue,
- textButtonInvertedHover: centrifugeBlue,
- textButtonInvertedPressed: centrifugeBlue,
+ textButtonInverted: black,
+ textButtonInvertedFocus: black,
+ textButtonInvertedHover: black,
+ textButtonInvertedPressed: black,
textButtonInvertedDisabled: grayScale[500],
borderButtonInverted: grayScale[100],
- borderButtonInvertedFocus: centrifugeBlue,
- borderButtonInvertedHover: centrifugeBlue,
- borderButtonInvertedPressed: centrifugeBlue,
+ borderButtonInvertedFocus: black,
+ borderButtonInvertedHover: black,
+ borderButtonInvertedPressed: black,
borderButtonInvertedDisabled: 'transparent',
- shadowButtonInverted: '#E0E7FF',
+ shadowButtonInverted: 'transparent',
}
export const colorTheme = {
diff --git a/fabric/src/theme/tokens/typography.ts b/fabric/src/theme/tokens/typography.ts
index fb917350a5..73ab39c964 100644
--- a/fabric/src/theme/tokens/typography.ts
+++ b/fabric/src/theme/tokens/typography.ts
@@ -80,7 +80,7 @@ const typography: ThemeTypography = {
label2: {
fontSize: 12,
lineHeight: 1.375,
- fontWeight: 400,
+ fontWeight: 500,
color: 'textSecondary',
},
}