diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 3a5b5765c..100050ca7 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -687,6 +687,13 @@ "min_balance_switch": "Minimum Balance", "min_balance_input": "Set a minimum balance for assets to show up in the table." } + }, + "positions": { + "title": "Active Positions", + "derivative": "Derivative", + "margin": "Margin", + "position": "Position", + "amount": "Amount" } }, "symbols": { diff --git a/src/components/AppSummary/AppSummary.columns.js b/src/components/AppSummary/AppSummary.columns.js index 431b74814..d19c0c244 100644 --- a/src/components/AppSummary/AppSummary.columns.js +++ b/src/components/AppSummary/AppSummary.columns.js @@ -1,9 +1,10 @@ import React from 'react' import { Cell } from '@blueprintjs/table' -import { fixedFloat } from 'ui/utils' +import { formatAmount, fixedFloat } from 'ui/utils' import LoadingPlaceholder from 'ui/LoadingPlaceholder' import { + getCell, getCellLoader, getCellNoData, getColumnWidth, @@ -12,10 +13,12 @@ import { import { getIsTotal, + getPairLabel, formatUsdValue, getFeePercentCell, formatPercentValue, formatUsdValueChange, + formatSecondaryPercentValue, } from './AppSummary.helpers' export const getFeesColumns = ({ @@ -302,3 +305,123 @@ export const getAssetColumns = ({ copyText: rowIndex => fixedFloat(preparedData[rowIndex]?.marginFundingPayment, 2), }, ] + +export const getPositionsColumns = ({ + t, + isNoData, + isLoading, + entries, + columnsWidth, +}) => [ + { + id: 'pair', + name: 'column.pair', + className: 'align-left', + width: getColumnWidth('pair', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData(t('column.noResults')) + const { pair, leverage } = entries[rowIndex] + const pairLabel = getPairLabel(t, pair, leverage) + return ( + + <> + + {pair} + +
+ + {pairLabel} + + +
+ ) + }, + copyText: rowIndex => entries[rowIndex].pair, + }, + { + id: 'amount', + name: 'column.amount', + width: getColumnWidth('amount', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData() + const { amount, basePrice } = entries[rowIndex] + return ( + + <> + + {fixedFloat(amount)} + +
+ + @ + {fixedFloat(basePrice)} + + +
+ ) + }, + copyText: rowIndex => fixedFloat(entries[rowIndex].amount), + }, + { + id: 'pl', + name: 'column.pl', + width: getColumnWidth('pl', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData() + const { pl, plPerc } = entries[rowIndex] + return ( + + <> + + {formatAmount(pl)} + +
+ + {formatSecondaryPercentValue(plPerc)} + + +
+ ) + }, + copyText: rowIndex => fixedFloat(entries[rowIndex].pl), + }, + { + id: 'liquidationPrice', + name: 'column.liq-price', + width: getColumnWidth('liquidationPrice', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData() + const { liquidationPrice } = entries[rowIndex] + return getCell(formatAmount(liquidationPrice, { color: 'red' }), t, fixedFloat(liquidationPrice)) + }, + copyText: rowIndex => fixedFloat(entries[rowIndex].liquidationPrice), + }, + { + id: 'marginFunding', + name: 'column.fundingCost', + width: getColumnWidth('marginFunding', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData() + const { marginFunding } = entries[rowIndex] + return getCell(fixedFloat(marginFunding), t) + }, + copyText: rowIndex => fixedFloat(entries[rowIndex].marginFunding), + }, + { + id: 'collateral', + name: 'column.collateral', + width: getColumnWidth('collateral', columnsWidth), + renderer: (rowIndex) => { + if (isLoading) return getCellLoader(22, 80) + if (isNoData) return getCellNoData() + const { collateral } = entries[rowIndex] + return getCell(`$${fixedFloat(collateral)}`, t) + }, + copyText: rowIndex => fixedFloat(entries[rowIndex].collateral), + }, +] diff --git a/src/components/AppSummary/AppSummary.container.js b/src/components/AppSummary/AppSummary.container.js index 8d86f3856..661ea3261 100644 --- a/src/components/AppSummary/AppSummary.container.js +++ b/src/components/AppSummary/AppSummary.container.js @@ -13,6 +13,7 @@ import { } from 'state/accountBalance/actions' import { refresh as refreshSummaryByAsset } from 'state/summaryByAsset/actions' import { refresh as refreshProfits } from 'state/profits/actions' +import { refresh as refreshPositions } from 'state/positionsActive/actions' import { getData, getPageLoading, @@ -44,6 +45,7 @@ const mapDispatchToProps = { setParams, refreshBalance, refreshProfits, + refreshPositions, refreshSummaryByAsset, } diff --git a/src/components/AppSummary/AppSummary.helpers.js b/src/components/AppSummary/AppSummary.helpers.js index aa285c26b..c8e975e18 100644 --- a/src/components/AppSummary/AppSummary.helpers.js +++ b/src/components/AppSummary/AppSummary.helpers.js @@ -1,5 +1,7 @@ import React from 'react' import { Cell } from '@blueprintjs/table' +import _isNil from 'lodash/isNil' +import _endsWith from 'lodash/endsWith' import LoadingPlaceholder from 'ui/LoadingPlaceholder' import { fixedFloat, formatFee, formatThousands } from 'ui/utils' @@ -60,3 +62,16 @@ export const getFeePercentCell = (isLoading, value) => ( )} ) + +export const formatSecondaryPercentValue = (value) => { + const val = prepareNumericValue(value) + if (val > 1) return {`${val}%`} + if (val < 1) return {`${val}%`} + return {`${val}%`} +} + +export const getPairLabel = (t, pair, leverage) => { + if (_endsWith(pair, 'PERP')) return t('summary.positions.derivative') + if (_isNil(leverage)) return t('summary.positions.position') + return t('summary.positions.margin') +} diff --git a/src/components/AppSummary/AppSummary.js b/src/components/AppSummary/AppSummary.js index c8465a53d..5d1a538f1 100644 --- a/src/components/AppSummary/AppSummary.js +++ b/src/components/AppSummary/AppSummary.js @@ -19,6 +19,7 @@ import Fees from './AppSummary.fees' import Value from './AppSummary.value' import Profits from './AppSummary.profits' import ByAsset from './AppSummary.byAsset' +import Positions from './AppSummary.positions' const AppSummary = ({ t, @@ -33,6 +34,7 @@ const AppSummary = ({ isSyncRequired, refreshBalance, refreshProfits, + refreshPositions, currentTimeFrame, refreshSummaryByAsset, isUnrealizedProfitExcluded, @@ -55,8 +57,9 @@ const AppSummary = ({ refresh() refreshBalance() refreshProfits() + refreshPositions() refreshSummaryByAsset() - }, [refresh, refreshBalance, refreshSummaryByAsset, refreshProfits]) + }, [refresh, refreshBalance, refreshSummaryByAsset, refreshProfits, refreshPositions]) return ( +
{ + const { t } = useTranslation() + const dispatch = useDispatch() + const entries = useSelector(getEntries) + const pageLoading = useSelector(getPageLoading) + const dataReceived = useSelector(getDataReceived) + const isFirstSync = useSelector(getIsFirstSyncing) + const isSyncRequired = useSelector(getIsSyncRequired) + const columnsWidth = useSelector((state) => getColumnsWidth(state, TYPE)) + const isLoading = isFirstSync || (!dataReceived && pageLoading) + const isNoData = dataReceived && isEmpty(entries) + const tableClasses = classNames('summary-positions-table', { + 'empty-table': isNoData, + }) + + useEffect(() => { + if (!dataReceived && !pageLoading && !isSyncRequired) { + dispatch(fetchAPositions()) + } + }, [dataReceived, pageLoading, isSyncRequired]) + + + const columns = useMemo( + () => getPositionsColumns({ + entries, t, isLoading, isNoData, columnsWidth, + }), + [entries, t, isLoading, isNoData, columnsWidth], + ) + + let showContent + if (isNoData) { + showContent = ( + + ) + } else { + showContent = ( + + ) + } + + return ( +
+
+
+
+ {t('summary.positions.title')} +
+
+
+ {showContent} +
+ ) +} + +export default SummaryActivePositions diff --git a/src/components/AppSummary/_AppSummary.scss b/src/components/AppSummary/_AppSummary.scss index 7928313ed..97645cce1 100644 --- a/src/components/AppSummary/_AppSummary.scss +++ b/src/components/AppSummary/_AppSummary.scss @@ -282,6 +282,11 @@ .collapsed-table { .secondary-value { color: var(--color2); + + &-left { + text-align: left; + color: var(--color2); + } } } @@ -340,6 +345,56 @@ } } + .summary-positions-table { + border-top: none; + + .bp3-table-column-name { + font-size: 14px; + + &-text { + padding: 0 5px 0 2px; + } + } + + .bp3-table-cell { + font-size: 14px; + padding: 0 5px 0 2px; + + &:nth-last-child(-n+6) { + box-shadow: none; + } + } + + .cell-value { + width: 100%; + text-align: end; + display: inline-block; + + &.secondary-value { + color: var(--color2); + + &-left { + text-align: left; + color: var(--color2); + } + } + } + + &.empty-table { + .bp3-table-cell { + &:nth-last-child(-n+6) { + box-shadow: inset 0 -1px 0 var(--tableBorder); + } + + &:nth-last-child(6) { + .bp3-table-truncated-text { + font-weight: 400; + } + } + } + } + } + .app-summary-item-sub-title { margin-bottom: 0; } @@ -457,6 +512,14 @@ } } +.percent-pos-value { + color: var(--tableAmountFractionPosColor); +} + +.percent-neg-value { + color: var(--tableAmountFractionNegColor); +} + @media screen and (max-width: 768px) { .full-width-item .app-summary-item-sub-title { margin-bottom: 15px; diff --git a/src/state/query/constants.js b/src/state/query/constants.js index 01b05f56c..6fea79570 100644 --- a/src/state/query/constants.js +++ b/src/state/query/constants.js @@ -34,6 +34,7 @@ export default { MENU_WIN_LOSS: 'averagewinloss', MENU_WEIGHTED_AVERAGES: 'weightedaverages', SUMMARY_BY_ASSET: 'summarybyasset', + SUMMARY_POSITIONS: 'summarypositions', TIME_TYPE_UTC: 'utc', TIME_TYPE_LOCALTIME: 'local', DEFAULT_QUERY_LIMIT: 500,