Feature/473 position margin levels (#902)

* feat(#473): add positions metrics data provider

* feat(#473) add positions stats

* feat(#473) add positions stats

* feat(#473): add positions stats

* feat(#473): add positions stats

* feat(#473): position metrics, test and refactoring

* feat(#473): add unit tests to positions table

* feat(#473): fix spelling, order positions by updated at desc

* feat(#473): protect from division by 0

* feat(#473): fix trading positions e2e tests

* feat(#473): fix e2e data mocks

* feat(#473): post code review clean up
This commit is contained in:
Bartłomiej Głownia 2022-08-16 09:18:55 +02:00 committed by GitHub
parent aaedbdde8c
commit 08b7c9769a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2035 additions and 484 deletions

View File

@ -18,7 +18,7 @@ describe('positions', () => {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('[col-id="market.tradableInstrument.instrument.code"]')
.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');

View File

@ -0,0 +1,163 @@
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import type {
PositionsMetrics,
PositionsMetrics_party_positionsConnection_edges_node,
} from '@vegaprotocol/positions';
import { MarketTradingMode, AccountType } from '@vegaprotocol/types';
export const generatePositionsMetrics = (
override?: PartialDeep<PositionsMetrics>
): PositionsMetrics => {
const nodes: PositionsMetrics_party_positionsConnection_edges_node[] = [
{
realisedPNL: '0',
openVolume: '6',
unrealisedPNL: '895000',
averageEntryPrice: '1129935',
updatedAt: '2022-07-28T15:09:34.441143Z',
market: {
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
name: 'UNIDAI Monthly (30 Jun 2022)',
tradingMode: MarketTradingMode.Continuous,
data: {
markPrice: '17588787',
__typename: 'MarketData',
},
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'UNIDAI Monthly (30 Jun 2022)',
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
__typename: 'Market',
},
__typename: 'Position',
},
{
realisedPNL: '0',
openVolume: '1',
unrealisedPNL: '-22519',
averageEntryPrice: '84400088',
updatedAt: '2022-07-28T14:53:54.725477Z',
market: {
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
name: 'Tesla Quarterly (30 Jun 2022)',
tradingMode: MarketTradingMode.Continuous,
data: {
markPrice: '84377569',
__typename: 'MarketData',
},
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'Tesla Quarterly (30 Jun 2022)',
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
__typename: 'Market',
},
__typename: 'Position',
},
];
const defaultResult: PositionsMetrics = {
party: {
id: Cypress.env('VEGA_PUBLIC_KEY'),
accounts: [
{
__typename: 'Account',
type: AccountType.General,
balance: '100000000',
market: null,
asset: {
__typename: 'Asset',
id: 'asset-id-2',
decimals: 5,
},
},
{
__typename: 'Account',
type: AccountType.Margin,
balance: '1000',
market: {
__typename: 'Market',
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
},
asset: {
__typename: 'Asset',
id: 'asset-id',
decimals: 5,
},
},
{
__typename: 'Account',
type: AccountType.Margin,
balance: '1000',
market: {
__typename: 'Market',
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
},
asset: {
__typename: 'Asset',
id: 'asset-id-2',
decimals: 5,
},
},
],
marginsConnection: {
__typename: 'MarginConnection',
edges: [
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
},
asset: {
__typename: 'Asset',
symbol: 'tDAI',
},
},
},
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376 ',
},
asset: {
__typename: 'Asset',
symbol: 'tEURO',
},
},
},
],
},
positionsConnection: {
__typename: 'PositionConnection',
edges: nodes.map((node) => ({ __typename: 'PositionEdge', node })),
},
__typename: 'Party',
},
};
return merge(defaultResult, override);
};

View File

@ -9,6 +9,7 @@ import { generateMarket } from './mocks/generate-market';
import { generateMarketInfoQuery } from './mocks/generate-market-info-query';
import { generateOrders } from './mocks/generate-orders';
import { generatePositions } from './mocks/generate-positions';
import { generatePositionsMetrics } from './mocks/generate-positions-metrics';
import { generateTrades } from './mocks/generate-trades';
export const mockTradingPage = (
@ -28,6 +29,7 @@ export const mockTradingPage = (
aliasQuery(req, 'Orders', generateOrders());
aliasQuery(req, 'Accounts', generateAccounts());
aliasQuery(req, 'Positions', generatePositions());
aliasQuery(req, 'PositionsMetrics', generatePositionsMetrics());
aliasQuery(
req,
'DealTicketQuery',

View File

@ -0,0 +1,28 @@
const rootMain = require('../../../.storybook/main');
module.exports = {
...rootMain,
core: { ...rootMain.core, builder: 'webpack5' },
stories: [
...rootMain.stories,
'../src/lib/**/*.stories.mdx',
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
...rootMain.addons,
'@nrwl/react/plugins/storybook',
'storybook-addon-themes',
],
webpackFinal: async (config, { configType }) => {
// apply any global webpack configs that might have been specified in .storybook/main.js
if (rootMain.webpackFinal) {
config = await rootMain.webpackFinal(config, { configType });
}
// add your own webpack tweaks if needed
return config;
},
};

View File

@ -0,0 +1 @@
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />

View File

@ -0,0 +1,50 @@
import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true },
themes: {
default: 'dark',
list: [
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
{ name: 'light', class: '', color: '#FFF' },
],
},
};
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesnt seem to provide the current selected
// theme in context, we need to provid it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, []);
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
</div>
);
},
];

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,19 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"outDir": ""
},
"files": [
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"../**/*.spec.ts",
"../**/*.spec.js",
"../**/*.spec.tsx",
"../**/*.spec.jsx"
],
"include": ["../src/**/*", "*.js"]
}

View File

@ -0,0 +1,10 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -38,6 +38,37 @@
"jestConfig": "libs/positions/jest.config.js",
"passWithNoTests": true
}
},
"storybook": {
"executor": "@nrwl/storybook:storybook",
"options": {
"uiFramework": "@storybook/react",
"port": 4400,
"config": {
"configFolder": "libs/positions/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nrwl/storybook:build",
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/react",
"outputPath": "dist/storybook/positions",
"config": {
"configFolder": "libs/positions/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}

View File

@ -1,5 +1,6 @@
export * from './lib/positions-table';
export * from './lib/positions-container';
export { positionsMetricsDataProvider } from './lib/positions-metrics-data-provider';
export * from './lib/positions-data-provider';
export * from './lib/__generated__/PositionsMetrics';
export * from './lib/__generated__/Positions';
export * from './lib/__generated__/PositionDetails';

View File

@ -33,68 +33,16 @@ export interface PositionDetails_market_data {
market: PositionDetails_market_data_market;
}
export interface PositionDetails_market_tradableInstrument_instrument_metadata {
__typename: "InstrumentMetadata";
/**
* An arbitrary list of tags to associated to associate to the Instrument (string list)
*/
tags: string[] | null;
}
export interface PositionDetails_market_tradableInstrument_instrument_product_settlementAsset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface PositionDetails_market_tradableInstrument_instrument_product {
__typename: "Future";
/**
* The name of the asset (string)
*/
settlementAsset: PositionDetails_market_tradableInstrument_instrument_product_settlementAsset;
/**
* String representing the quote (e.g. BTCUSD -> USD is quote)
*/
quoteName: string;
}
export interface PositionDetails_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Uniquely identify an instrument across all instruments available on Vega (string)
*/
id: string;
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* Metadata for this instrument
*/
metadata: PositionDetails_market_tradableInstrument_instrument_metadata;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
product: PositionDetails_market_tradableInstrument_instrument_product;
}
export interface PositionDetails_market_tradableInstrument {

View File

@ -0,0 +1,109 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL fragment: PositionMetricsFields
// ====================================================
export interface PositionMetricsFields_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
}
export interface PositionMetricsFields_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: PositionMetricsFields_market_tradableInstrument_instrument;
}
export interface PositionMetricsFields_market_data {
__typename: "MarketData";
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface PositionMetricsFields_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
*/
positionDecimalPlaces: number;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: PositionMetricsFields_market_tradableInstrument;
/**
* marketData for the given market
*/
data: PositionMetricsFields_market_data | null;
}
export interface PositionMetricsFields {
__typename: "Position";
/**
* Realised Profit and Loss (int64)
*/
realisedPNL: string;
/**
* Open volume (uint64)
*/
openVolume: string;
/**
* Unrealised Profit and Loss (int64)
*/
unrealisedPNL: string;
/**
* Average entry price for this position
*/
averageEntryPrice: string;
/**
* RFC3339Nano time the position was updated
*/
updatedAt: string | null;
/**
* Market relating to this position
*/
market: PositionMetricsFields_market;
}

View File

@ -33,68 +33,16 @@ export interface PositionSubscribe_positions_market_data {
market: PositionSubscribe_positions_market_data_market;
}
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_metadata {
__typename: "InstrumentMetadata";
/**
* An arbitrary list of tags to associated to associate to the Instrument (string list)
*/
tags: string[] | null;
}
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_product_settlementAsset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_product {
__typename: "Future";
/**
* The name of the asset (string)
*/
settlementAsset: PositionSubscribe_positions_market_tradableInstrument_instrument_product_settlementAsset;
/**
* String representing the quote (e.g. BTCUSD -> USD is quote)
*/
quoteName: string;
}
export interface PositionSubscribe_positions_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Uniquely identify an instrument across all instruments available on Vega (string)
*/
id: string;
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* Metadata for this instrument
*/
metadata: PositionSubscribe_positions_market_tradableInstrument_instrument_metadata;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
product: PositionSubscribe_positions_market_tradableInstrument_instrument_product;
}
export interface PositionSubscribe_positions_market_tradableInstrument {

View File

@ -33,68 +33,16 @@ export interface Positions_party_positions_market_data {
market: Positions_party_positions_market_data_market;
}
export interface Positions_party_positions_market_tradableInstrument_instrument_metadata {
__typename: "InstrumentMetadata";
/**
* An arbitrary list of tags to associated to associate to the Instrument (string list)
*/
tags: string[] | null;
}
export interface Positions_party_positions_market_tradableInstrument_instrument_product_settlementAsset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface Positions_party_positions_market_tradableInstrument_instrument_product {
__typename: "Future";
/**
* The name of the asset (string)
*/
settlementAsset: Positions_party_positions_market_tradableInstrument_instrument_product_settlementAsset;
/**
* String representing the quote (e.g. BTCUSD -> USD is quote)
*/
quoteName: string;
}
export interface Positions_party_positions_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Uniquely identify an instrument across all instruments available on Vega (string)
*/
id: string;
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* Metadata for this instrument
*/
metadata: Positions_party_positions_market_tradableInstrument_instrument_metadata;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
/**
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
*/
product: Positions_party_positions_market_tradableInstrument_instrument_product;
}
export interface Positions_party_positions_market_tradableInstrument {

View File

@ -0,0 +1,251 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType, MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: PositionsMetrics
// ====================================================
export interface PositionsMetrics_party_accounts_asset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface PositionsMetrics_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PositionsMetrics_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Asset, the 'currency'
*/
asset: PositionsMetrics_party_accounts_asset;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Market (only relevant to margin accounts)
*/
market: PositionsMetrics_party_accounts_market | null;
}
export interface PositionsMetrics_party_marginsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PositionsMetrics_party_marginsConnection_edges_node_asset {
__typename: "Asset";
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
}
export interface PositionsMetrics_party_marginsConnection_edges_node {
__typename: "MarginLevels";
/**
* market in which the margin is required for this party
*/
market: PositionsMetrics_party_marginsConnection_edges_node_market;
/**
* minimal margin for the position to be maintained in the network (unsigned int actually)
*/
maintenanceLevel: string;
/**
* if the margin is between maintenance and search, the network will initiate a collateral search (unsigned int actually)
*/
searchLevel: string;
/**
* this is the minimal margin required for a party to place a new order on the network (unsigned int actually)
*/
initialLevel: string;
/**
* If the margin of the party is greater than this level, then collateral will be released from the margin account into
* the general account of the party for the given asset.
*/
collateralReleaseLevel: string;
/**
* asset for the current margins
*/
asset: PositionsMetrics_party_marginsConnection_edges_node_asset;
}
export interface PositionsMetrics_party_marginsConnection_edges {
__typename: "MarginEdge";
node: PositionsMetrics_party_marginsConnection_edges_node;
}
export interface PositionsMetrics_party_marginsConnection {
__typename: "MarginConnection";
/**
* The margin levels in this connection
*/
edges: PositionsMetrics_party_marginsConnection_edges[] | null;
}
export interface PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
}
export interface PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument_instrument;
}
export interface PositionsMetrics_party_positionsConnection_edges_node_market_data {
__typename: "MarketData";
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface PositionsMetrics_party_positionsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
*/
positionDecimalPlaces: number;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument;
/**
* marketData for the given market
*/
data: PositionsMetrics_party_positionsConnection_edges_node_market_data | null;
}
export interface PositionsMetrics_party_positionsConnection_edges_node {
__typename: "Position";
/**
* Realised Profit and Loss (int64)
*/
realisedPNL: string;
/**
* Open volume (uint64)
*/
openVolume: string;
/**
* Unrealised Profit and Loss (int64)
*/
unrealisedPNL: string;
/**
* Average entry price for this position
*/
averageEntryPrice: string;
/**
* RFC3339Nano time the position was updated
*/
updatedAt: string | null;
/**
* Market relating to this position
*/
market: PositionsMetrics_party_positionsConnection_edges_node_market;
}
export interface PositionsMetrics_party_positionsConnection_edges {
__typename: "PositionEdge";
node: PositionsMetrics_party_positionsConnection_edges_node;
}
export interface PositionsMetrics_party_positionsConnection {
__typename: "PositionConnection";
/**
* The positions in this connection
*/
edges: PositionsMetrics_party_positionsConnection_edges[] | null;
}
export interface PositionsMetrics_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: PositionsMetrics_party_accounts[] | null;
/**
* Margin level for a market
*/
marginsConnection: PositionsMetrics_party_marginsConnection;
/**
* Trading positions relating to a party
*/
positionsConnection: PositionsMetrics_party_positionsConnection;
}
export interface PositionsMetrics {
/**
* An entity that is trading on the VEGA network
*/
party: PositionsMetrics_party | null;
}
export interface PositionsMetricsVariables {
partyId: string;
}

View File

@ -0,0 +1,120 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { MarketTradingMode } from "@vegaprotocol/types";
// ====================================================
// GraphQL subscription operation: PositionsMetricsSubscription
// ====================================================
export interface PositionsMetricsSubscription_positions_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
}
export interface PositionsMetricsSubscription_positions_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: PositionsMetricsSubscription_positions_market_tradableInstrument_instrument;
}
export interface PositionsMetricsSubscription_positions_market_data {
__typename: "MarketData";
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface PositionsMetricsSubscription_positions_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
*/
positionDecimalPlaces: number;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: PositionsMetricsSubscription_positions_market_tradableInstrument;
/**
* marketData for the given market
*/
data: PositionsMetricsSubscription_positions_market_data | null;
}
export interface PositionsMetricsSubscription_positions {
__typename: "Position";
/**
* Realised Profit and Loss (int64)
*/
realisedPNL: string;
/**
* Open volume (uint64)
*/
openVolume: string;
/**
* Unrealised Profit and Loss (int64)
*/
unrealisedPNL: string;
/**
* Average entry price for this position
*/
averageEntryPrice: string;
/**
* RFC3339Nano time the position was updated
*/
updatedAt: string | null;
/**
* Market relating to this position
*/
market: PositionsMetricsSubscription_positions_market;
}
export interface PositionsMetricsSubscription {
/**
* Subscribe to the positions updates
*/
positions: PositionsMetricsSubscription_positions;
}
export interface PositionsMetricsSubscriptionVariables {
partyId: string;
}

View File

@ -31,23 +31,8 @@ const POSITIONS_FRAGMENT = gql`
positionDecimalPlaces
tradableInstrument {
instrument {
id
name
metadata {
tags
}
code
product {
... on Future {
settlementAsset {
id
symbol
name
decimals
}
quoteName
}
}
}
}
}

View File

@ -1,14 +1,12 @@
import { useRef, useCallback, useMemo } from 'react';
import { produce } from 'immer';
import merge from 'lodash/merge';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { PositionSubscribe_positions } from './__generated__/PositionSubscribe';
import type { Positions_party_positions } from './__generated__/Positions';
import type { PositionsMetricsSubscription_positions } from './__generated__/PositionsMetricsSubscription';
import type { AgGridReact } from 'ag-grid-react';
import PositionsTable, { getRowId } from './positions-table';
import { positionsDataProvider } from './positions-data-provider';
import PositionsTable from './positions-table';
import type { GetRowsParams } from './positions-table';
import { positionsMetricsDataProvider as dataProvider } from './positions-metrics-data-provider';
import type { Data, Position } from './positions-metrics-data-provider';
interface PositionsManagerProps {
partyId: string;
@ -17,45 +15,39 @@ interface PositionsManagerProps {
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
const update = useCallback(
({ delta }: { delta: PositionSubscribe_positions }) => {
const update: Positions_party_positions[] = [];
const add: Positions_party_positions[] = [];
if (!gridRef.current?.api) {
return false;
}
const rowNode = gridRef.current.api.getRowNode(getRowId({ data: delta }));
if (rowNode) {
const updatedData = produce<Positions_party_positions>(
rowNode.data,
(draft: Positions_party_positions) => {
merge(draft, delta);
}
);
if (updatedData !== rowNode.data) {
update.push(updatedData);
}
} else {
add.push(delta);
}
if (update.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
add,
addIndex: 0,
});
}
return true;
},
[gridRef]
);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(({ data }: { data: Data }) => {
if (!gridRef.current?.api) {
return false;
}
dataRef.current = data.positions;
gridRef.current.api.refreshInfiniteCache();
return true;
}, []);
const { data, error, loading } = useDataProvider<
Positions_party_positions[],
PositionSubscribe_positions
>({ dataProvider: positionsDataProvider, update, variables });
Data,
PositionsMetricsSubscription_positions
>({ dataProvider, update, variables });
dataRef.current = data?.positions || null;
const getRows = async ({
successCallback,
startRow,
endRow,
}: GetRowsParams) => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current?.length ?? -1;
successCallback(rowsThisBlock, lastRow);
};
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<PositionsTable ref={gridRef} data={data} />
<PositionsTable
rowModelType={data?.positions?.length ? 'infinite' : 'clientSide'}
rowData={data?.positions?.length ? undefined : []}
datasource={{ getRows }}
/>
</AsyncRenderer>
);
};

View File

@ -0,0 +1,214 @@
import { AccountType, MarketTradingMode } from '@vegaprotocol/types';
import type { PositionsMetrics } from './__generated__/PositionsMetrics';
import { getMetrics } from './positions-metrics-data-provider';
const data: PositionsMetrics = {
party: {
__typename: 'Party',
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
accounts: [
{
__typename: 'Account',
type: AccountType.General,
asset: {
__typename: 'Asset',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
balance: '892824769',
market: null,
},
{
__typename: 'Account',
type: AccountType.Margin,
asset: {
__typename: 'Asset',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
balance: '33353727',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
},
{
__typename: 'Account',
type: AccountType.Margin,
asset: {
__typename: 'Asset',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
balance: '3274050',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
},
],
marginsConnection: {
__typename: 'MarginConnection',
edges: [
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
symbol: 'tDAI',
},
},
},
{
__typename: 'MarginEdge',
node: {
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
symbol: 'tDAI',
},
},
},
],
},
positionsConnection: {
__typename: 'PositionConnection',
edges: [
{
__typename: 'PositionEdge',
node: {
__typename: 'Position',
openVolume: '100',
averageEntryPrice: '8993727',
updatedAt: '2022-07-28T14:53:54.725477Z',
realisedPNL: '0',
unrealisedPNL: '43804770',
market: {
__typename: 'Market',
name: 'AAVEDAI Monthly (30 Jun 2022)',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
decimalPlaces: 5,
tradingMode: MarketTradingMode.MonitoringAuction,
positionDecimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'AAVEDAI Monthly (30 Jun 2022)',
},
},
data: {
__typename: 'MarketData',
markPrice: '9431775',
},
},
},
},
{
__typename: 'PositionEdge',
node: {
__typename: 'Position',
openVolume: '-100',
realisedPNL: '0',
unrealisedPNL: '-9112700',
averageEntryPrice: '840158',
updatedAt: '2022-07-28T15:09:34.441143Z',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
tradingMode: MarketTradingMode.Continuous,
positionDecimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'UNIDAI Monthly (30 Jun 2022)',
},
},
data: {
__typename: 'MarketData',
markPrice: '869762',
},
},
},
},
],
},
},
};
describe('getMetrics', () => {
it('returns positions metrics', () => {
const metrics = getMetrics(data.party);
expect(metrics.length).toEqual(2);
});
it('calculates metrics', () => {
const metrics = getMetrics(data.party);
expect(metrics[0].assetSymbol).toEqual('tDAI');
expect(metrics[0].averageEntryPrice).toEqual('8993727');
expect(metrics[0].capitalUtilisation).toEqual(4);
expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].assetDecimals).toEqual(5);
expect(metrics[0].liquidationPrice).toEqual('169990');
expect(metrics[0].lowMarginLevel).toEqual(false);
expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
);
expect(metrics[0].marketName).toEqual('AAVEDAI Monthly (30 Jun 2022)');
expect(metrics[0].marketTradingMode).toEqual('MonitoringAuction');
expect(metrics[0].notional).toEqual('943177500');
expect(metrics[0].openVolume).toEqual('100');
expect(metrics[0].realisedPNL).toEqual('0');
expect(metrics[0].searchPrice).toEqual('9098238');
expect(metrics[0].totalBalance).toEqual('926178496');
expect(metrics[0].unrealisedPNL).toEqual('43804770');
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
expect(metrics[1].assetSymbol).toEqual('tDAI');
expect(metrics[1].averageEntryPrice).toEqual('840158');
expect(metrics[1].capitalUtilisation).toEqual(0);
expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].assetDecimals).toEqual(5);
expect(metrics[1].liquidationPrice).toEqual('9830750');
expect(metrics[1].lowMarginLevel).toEqual(false);
expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
);
expect(metrics[1].marketName).toEqual('UNIDAI Monthly (30 Jun 2022)');
expect(metrics[1].marketTradingMode).toEqual('Continuous');
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
expect(metrics[1].realisedPNL).toEqual('0');
expect(metrics[1].searchPrice).toEqual('902503');
expect(metrics[1].totalBalance).toEqual('896098819');
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
});
});

View File

@ -0,0 +1,285 @@
import { gql } from '@apollo/client';
import produce from 'immer';
import BigNumber from 'bignumber.js';
import sortBy from 'lodash/sortBy';
import type {
PositionsMetrics,
PositionsMetrics_party,
} from './__generated__/PositionsMetrics';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type {
PositionsMetricsSubscription,
PositionsMetricsSubscription_positions,
} from './__generated__/PositionsMetricsSubscription';
import { AccountType } from '@vegaprotocol/types';
import type { MarketTradingMode } from '@vegaprotocol/types';
export interface Position {
marketName: string;
averageEntryPrice: string;
capitalUtilisation: number;
currentLeverage: number;
assetDecimals: number;
marketDecimalPlaces: number;
positionDecimalPlaces: number;
totalBalance: string;
assetSymbol: string;
liquidationPrice: string;
lowMarginLevel: boolean;
marketId: string;
marketTradingMode: MarketTradingMode;
markPrice: string;
notional: string;
openVolume: string;
realisedPNL: string;
unrealisedPNL: string;
searchPrice: string;
updatedAt: string | null;
}
export interface Data {
party: PositionsMetrics_party | null;
positions: Position[] | null;
}
const POSITIONS_METRICS_FRAGMENT = gql`
fragment PositionMetricsFields on Position {
realisedPNL
openVolume
unrealisedPNL
averageEntryPrice
updatedAt
market {
id
name
decimalPlaces
positionDecimalPlaces
tradingMode
tradableInstrument {
instrument {
name
}
}
data {
markPrice
}
}
}
`;
const POSITION_METRICS_QUERY = gql`
${POSITIONS_METRICS_FRAGMENT}
query PositionsMetrics($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
asset {
id
decimals
}
balance
market {
id
}
}
marginsConnection {
edges {
node {
market {
id
}
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
symbol
}
}
}
}
positionsConnection {
edges {
node {
...PositionMetricsFields
}
}
}
}
}
`;
export const POSITIONS_METRICS_SUBSCRIPTION = gql`
${POSITIONS_METRICS_FRAGMENT}
subscription PositionsMetricsSubscription($partyId: ID!) {
positions(partyId: $partyId) {
...PositionMetricsFields
}
}
`;
export const getMetrics = (data: PositionsMetrics_party | null): Position[] => {
if (!data || !data.positionsConnection.edges) {
return [];
}
const metrics: Position[] = [];
data.positionsConnection.edges.forEach((position) => {
const market = position.node.market;
const marketData = market.data;
const marginLevel = data.marginsConnection.edges?.find(
(margin) => margin.node.market.id === market.id
)?.node;
const marginAccount = data.accounts?.find(
(account) => account.market?.id === market.id
);
if (!marginAccount || !marginLevel || !marketData) {
return;
}
const generalAccount = data.accounts?.find(
(account) =>
account.asset.id === marginAccount.asset.id &&
account.type === AccountType.General
);
const assetDecimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market;
const openVolume = new BigNumber(position.node.openVolume).dividedBy(
10 ** positionDecimalPlaces
);
const marginAccountBalance = marginAccount
? new BigNumber(marginAccount.balance).dividedBy(10 ** assetDecimals)
: new BigNumber(0);
const generalAccountBalance = generalAccount
? new BigNumber(generalAccount.balance).dividedBy(10 ** assetDecimals)
: new BigNumber(0);
const markPrice = new BigNumber(marketData.markPrice).dividedBy(
10 ** marketDecimalPlaces
);
const notional = (
openVolume.isGreaterThan(0) ? openVolume : openVolume.multipliedBy(-1)
).multipliedBy(markPrice);
const totalBalance = marginAccountBalance.plus(generalAccountBalance);
const currentLeverage = totalBalance.isEqualTo(0)
? new BigNumber(0)
: notional.dividedBy(totalBalance);
const capitalUtilisation = totalBalance.isEqualTo(0)
? new BigNumber(0)
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
const marginMaintenance = new BigNumber(
marginLevel.maintenanceLevel
).multipliedBy(marketDecimalPlaces);
const marginSearch = new BigNumber(marginLevel.searchLevel).multipliedBy(
marketDecimalPlaces
);
const marginInitial = new BigNumber(marginLevel.initialLevel).multipliedBy(
marketDecimalPlaces
);
const searchPrice = openVolume.isEqualTo(0)
? markPrice
: marginSearch
.minus(marginAccountBalance)
.dividedBy(openVolume)
.plus(markPrice);
const liquidationPrice = openVolume.isEqualTo(0)
? markPrice
: marginMaintenance
.minus(marginAccountBalance)
.minus(generalAccountBalance)
.dividedBy(openVolume)
.plus(markPrice);
const lowMarginLevel =
marginAccountBalance.isLessThan(
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
metrics.push({
marketName: market.name,
averageEntryPrice: position.node.averageEntryPrice,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage.toNumber(),
marketDecimalPlaces,
positionDecimalPlaces,
assetDecimals,
assetSymbol: marginLevel.asset.symbol,
totalBalance: totalBalance.multipliedBy(10 ** assetDecimals).toFixed(),
lowMarginLevel,
liquidationPrice: liquidationPrice
.multipliedBy(10 ** marketDecimalPlaces)
.toFixed(0),
marketId: position.node.market.id,
marketTradingMode: position.node.market.tradingMode,
markPrice: marketData.markPrice,
notional: notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0),
openVolume: position.node.openVolume,
realisedPNL: position.node.realisedPNL,
unrealisedPNL: position.node.unrealisedPNL,
searchPrice: searchPrice
.multipliedBy(10 ** marketDecimalPlaces)
.toFixed(0),
updatedAt: position.node.updatedAt,
});
});
return metrics;
};
export const update = (
data: Data,
delta: PositionsMetricsSubscription_positions
) => {
if (!data.party?.positionsConnection.edges) {
return data;
}
const edges = produce(data.party.positionsConnection.edges, (draft) => {
const index = draft.findIndex(
(edge) => edge.node.market.id === delta.market.id
);
if (index !== -1) {
draft[index].node = delta;
} else {
draft.push({ __typename: 'PositionEdge', node: delta });
}
});
const party = produce(data.party, (draft) => {
draft.positionsConnection.edges = edges;
});
if (party === data.party) {
return data;
}
return {
party,
positions: getMetrics(party),
};
};
const getData = (responseData: PositionsMetrics): Data => {
return {
party: responseData.party,
positions: sortBy(getMetrics(responseData.party), 'updatedAt').reverse(),
};
};
const getDelta = (
subscriptionData: PositionsMetricsSubscription
): PositionsMetricsSubscription_positions => subscriptionData.positions;
export const positionsMetricsDataProvider = makeDataProvider<
PositionsMetrics,
Data,
PositionsMetricsSubscription,
PositionsMetricsSubscription_positions
>(
POSITION_METRICS_QUERY,
POSITIONS_METRICS_SUBSCRIPTION,
update,
getData,
getDelta
);

View File

@ -1,103 +1,176 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import type { RenderResult } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import PositionsTable from './positions-table';
import type { Positions_party_positions } from './__generated__/Positions';
import type { Position } from './positions-metrics-data-provider';
import { MarketTradingMode } from '@vegaprotocol/types';
const singleRow: Positions_party_positions = {
realisedPNL: '520000000',
openVolume: '10000',
unrealisedPNL: '895000',
averageEntryPrice: '1129935',
market: {
id: 'b7010da9dbe7fbab2b74d9d5642fc4a8a0ca93ef803d21fa60c2cacd0416bba0',
name: 'UNIDAI Monthly (30 Jun 2022)',
data: {
markPrice: '1138885',
marketTradingMode: MarketTradingMode.Continuous,
__typename: 'MarketData',
market: { __typename: 'Market', id: '123' },
},
positionDecimalPlaces: 2,
decimalPlaces: 5,
tradableInstrument: {
instrument: {
id: '',
name: 'UNIDAI Monthly (30 Jun 2022)',
metadata: {
tags: [
'formerly:3C58ED2A4A6C5D7E',
'base:UNI',
'quote:DAI',
'class:fx/crypto',
'monthly',
'sector:defi',
],
__typename: 'InstrumentMetadata',
},
code: 'UNIDAI.MF21',
product: {
settlementAsset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
decimals: 5,
__typename: 'Asset',
},
quoteName: 'DAI',
__typename: 'Future',
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
__typename: 'Market',
},
__typename: 'Position',
const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)',
averageEntryPrice: '133', // 13.3
capitalUtilisation: 11, // 11.00%
currentLeverage: 1.1,
marketDecimalPlaces: 1,
positionDecimalPlaces: 0,
assetDecimals: 2,
totalBalance: '123456',
assetSymbol: 'BTC',
liquidationPrice: '83', // 8.3
lowMarginLevel: false,
marketId: 'string',
marketTradingMode: MarketTradingMode.Continuous,
markPrice: '123', // 12.3
notional: '12300', // 1230.0
openVolume: '100', // 100
realisedPNL: '123', // 1.23
unrealisedPNL: '456', // 4.56
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
};
const singleRowData = [singleRow];
describe('PositionsTable', () => {
it('should render successfully', async () => {
act(async () => {
const { baseElement } = render(<PositionsTable data={[]} />);
expect(baseElement).toBeTruthy();
});
});
it('should render correct columns', async () => {
act(async () => {
render(<PositionsTable data={singleRowData} />);
await waitFor(async () => {
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(5);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Market',
'Size',
'Average Entry Price',
'Mark Price',
'Realised PNL',
]);
});
});
});
it('should apply correct formatting', () => {
act(async () => {
render(<PositionsTable data={singleRowData} />);
await waitFor(async () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
singleRow.market.tradableInstrument.instrument.code,
'+100.00',
'11.29935',
'11.38885',
'+5,200.000',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
expect(cells[cells.length - 1]).toHaveClass('text-vega-green-dark');
});
});
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(<PositionsTable rowData={[]} />);
expect(baseElement).toBeTruthy();
});
});
it('Render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(9);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Amount',
'Mark Price',
'Entry Price',
'Leverage',
'Margin allocated',
'Realised PNL',
'Unrealised PNL',
'Updated',
]);
});
it('Splits market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
expect(screen.getByText('ETH/BTC')).toBeTruthy();
expect(screen.getByText('31 july 2022')).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
});
let cells = screen.getAllByRole('gridcell');
let values = cells[1].querySelectorAll('.text-right');
expect(values[0].classList.contains('text-vega-green-dark')).toBeTruthy();
expect(values[0].classList.contains('text-vega-red-dark')).toBeFalsy();
expect(values[0].textContent).toEqual('+100');
expect(values[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} />
);
});
cells = screen.getAllByRole('gridcell');
values = cells[1].querySelectorAll('.text-right');
expect(values[0].classList.contains('text-vega-green-dark')).toBeFalsy();
expect(values[0].classList.contains('text-vega-red-dark')).toBeTruthy();
expect(values[0].textContent?.startsWith('-100')).toBeTruthy();
expect(values[1].textContent).toEqual('1,230.0');
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{ ...singleRow, marketTradingMode: MarketTradingMode.OpeningAuction },
]}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('-');
});
it("displays properly entry, liquidation price and liquidation bar and it's intent", async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
});
let cells = screen.getAllByRole('gridcell');
let cell = cells[3];
const entryPrice = cell.firstElementChild?.firstElementChild?.textContent;
const liquidationPrice =
cell.firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cell.lastElementChild;
let progressBar = progressBarTrack?.firstElementChild as HTMLElement;
const progressBarWidth = progressBar?.style?.width;
expect(entryPrice).toEqual('13.3');
expect(liquidationPrice).toEqual('8.3');
expect(progressBar.classList.contains('bg-danger')).toEqual(false);
expect(progressBarWidth).toEqual('20%');
await act(async () => {
result.rerender(
<PositionsTable rowData={[{ ...singleRow, lowMarginLevel: true }]} />
);
});
cells = screen.getAllByRole('gridcell');
cell = cells[3];
progressBar = cell.lastElementChild?.firstElementChild as HTMLElement;
expect(progressBar?.classList.contains('bg-danger')).toEqual(true);
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('1.1');
});
it('displays allocated margin and margin bar', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[5];
const capitalUtilisation =
cell.firstElementChild?.firstElementChild?.textContent;
const totalBalance = cell.firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cell.lastElementChild;
const progressBar = progressBarTrack?.firstElementChild as HTMLElement;
const progressBarWidth = progressBar?.style?.width;
expect(capitalUtilisation).toEqual('11.00%');
expect(totalBalance).toEqual('1,234.56');
expect(progressBarWidth).toEqual('11%');
});
it('displays realised and unrealised PNL', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[6].textContent).toEqual('1.23');
expect(cells[7].textContent).toEqual('4.56');
});

View File

@ -0,0 +1,80 @@
import type { Story, Meta } from '@storybook/react';
import { PositionsTable } from './positions-table';
import type { Position } from './positions-metrics-data-provider';
import { MarketTradingMode } from '@vegaprotocol/types';
export default {
component: PositionsTable,
title: 'PositionsTable',
} as Meta;
const Template: Story = (args) => <PositionsTable {...args} />;
export const Primary = Template.bind({});
const longPosition: Position = {
marketName: 'BTC/USD (31 july 2022)',
averageEntryPrice: '1134564',
capitalUtilisation: 10.0,
currentLeverage: 11,
assetDecimals: 2,
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '45353',
assetSymbol: 'BTC',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
liquidationPrice: '1129935',
lowMarginLevel: false,
// marginAccountBalance: '0',
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId1',
marketTradingMode: MarketTradingMode.Continuous,
markPrice: '1131894',
notional: '46667989',
openVolume: '4123',
realisedPNL: '45',
unrealisedPNL: '45',
searchPrice: '1132123',
updatedAt: '2022-07-27T15:02:58.400Z',
};
const shortPosition: Position = {
marketName: 'ETH/USD (31 august 2022)',
averageEntryPrice: '23976',
capitalUtilisation: 87.0,
currentLeverage: 7,
assetDecimals: 2,
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '3856',
assetSymbol: 'ETH',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
liquidationPrice: '23734',
lowMarginLevel: false,
// marginAccountBalance: '0',
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId2',
marketTradingMode: MarketTradingMode.Continuous,
markPrice: '24123',
notional: '836344',
openVolume: '-3467',
realisedPNL: '0',
unrealisedPNL: '0',
searchPrice: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
};
Primary.args = {
rowData: [longPosition, shortPosition],
};

View File

@ -1,186 +1,343 @@
import classNames from 'classnames';
import { forwardRef } from 'react';
import type { ValueFormatterParams } from 'ag-grid-community';
import {
PriceFlashCell,
addDecimalsFormatNumber,
volumePrefix,
addDecimal,
t,
formatNumber,
getDateTimeFormat,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, Tooltip } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Positions_party_positions } from './__generated__/Positions';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { Position } from './positions-metrics-data-provider';
import { MarketTradingMode } from '@vegaprotocol/types';
import { Intent } from '@vegaprotocol/ui-toolkit';
interface PositionsTableProps {
data: Positions_party_positions[] | null;
export const getRowId = ({ data }: { data: Position }) => data.marketId;
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: Position[], lastRow?: number): void;
}
type ColumnHeaderProps = {
displayName: string;
tooltipContent?: string;
};
const ColumnHeader = ({ displayName, tooltipContent }: ColumnHeaderProps) => {
if (tooltipContent) {
return (
<Tooltip description={tooltipContent}>
<span>{displayName}</span>
</Tooltip>
);
}
return displayName;
};
export const getRowId = ({ data }: { data: Positions_party_positions }) =>
data.market.id;
const alphanumericComparator = (a: string, b: string, isInverted: boolean) => {
if (a < b) {
return isInverted ? 1 : -1;
}
if (a > b) {
return isInverted ? -1 : 1;
}
return 0;
};
const comparator = (
valueA: string,
valueB: string,
nodeA: { data: Positions_party_positions },
nodeB: { data: Positions_party_positions },
isInverted: boolean
) =>
alphanumericComparator(
nodeA.data.market.tradableInstrument.instrument.name,
nodeB.data.market.tradableInstrument.instrument.name,
isInverted
) ||
alphanumericComparator(
nodeA.data.market.id,
nodeB.data.market.id,
isInverted
);
interface PositionsTableValueFormatterParams extends ValueFormatterParams {
data: Positions_party_positions;
export interface Datasource extends IDatasource {
getRows(params: GetRowsParams): void;
}
interface Props extends AgGridReactProps {
rowData?: Position[] | null;
datasource?: Datasource;
}
export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
({ data }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate="No positions"
rowData={data}
getRowId={getRowId}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
}}
onGridReady={(event) => {
event.columnApi.applyColumnState({
state: [
{
colId: 'market.tradableInstrument.instrument.code',
sort: 'asc',
},
],
});
}}
components={{ PriceFlashCell, agColumnHeader: ColumnHeader }}
type PositionsTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Position;
};
export interface MarketNameCellProps {
valueFormatted?: [string, string];
}
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
return valueFormatted ? (
<div className="leading-tight">
<div>{valueFormatted[0]}</div>
{valueFormatted[1] ? <div>{valueFormatted[1]}</div> : null}
</div>
) : null;
};
export interface PriceCellProps {
valueFormatted?: {
low: string;
high: string;
value: number;
intent?: Intent;
};
}
export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
return valueFormatted ? (
<>
<div className="flex justify-between leading-tight">
<div>{valueFormatted.low}</div>
<div>{valueFormatted.high}</div>
</div>
<ProgressBar
value={valueFormatted.value}
intent={valueFormatted.intent}
className="mt-4"
/>
</>
) : null;
};
ProgressBarCell.displayName = 'PriceFlashCell';
export interface AmountCellProps {
valueFormatted?: Pick<
Position,
'openVolume' | 'marketDecimalPlaces' | 'positionDecimalPlaces' | 'notional'
>;
}
export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
if (!valueFormatted) {
return null;
}
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
valueFormatted;
const isShortPosition = openVolume.startsWith('-');
return valueFormatted ? (
<div className="leading-tight">
<div
className={classNames('text-right', {
'text-vega-green-dark dark:text-vega-green': !isShortPosition,
'text-vega-red-dark dark:text-vega-red': isShortPosition,
})}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
comparator={comparator}
sortable
/>
<AgGridColumn
headerName={t('Size')}
field="openVolume"
cellClassRules={{
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => Number(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => Number(value) < 0,
}}
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams) =>
volumePrefix(addDecimal(value, data.market.positionDecimalPlaces))
{volumePrefix(
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
)}
</div>
<div className="text-right">
{addDecimalsFormatNumber(notional, marketDecimalPlaces)}
</div>
</div>
) : null;
};
AmountCell.displayName = 'AmountCell';
export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate="No positions"
getRowId={getRowId}
rowHeight={34}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
}}
components={{ PriceFlashCell, ProgressBarCell }}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer={MarketNameCell}
valueFormatter={({
value,
}: PositionsTableValueFormatterParams & {
value: Position['marketName'];
}) => {
if (!value) {
return undefined;
}
/>
<AgGridColumn
headerName={t('Average Entry Price')}
field="averageEntryPrice"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams) =>
addDecimalsFormatNumber(value, data.market.decimalPlaces)
// split market name into two parts, 'Part1 (Part2)'
const matches = value.match(/^(.*)\((.*)\)\s*$/);
if (matches) {
return [matches[1].trim(), matches[2].trim()];
}
/>
<AgGridColumn
headerName={t('Mark Price')}
field="market.data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams) => {
if (
data.market.data?.marketTradingMode ===
MarketTradingMode.OpeningAuction
) {
return '-';
}
return addDecimal(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
headerComponentParams={{
tooltipContent: t('P&L excludes any fees paid.'),
}}
cellClassRules={{
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => Number(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => Number(value) < 0,
}}
valueFormatter={({ value, data }: ValueFormatterParams) =>
volumePrefix(
addDecimalsFormatNumber(value, data.market.decimalPlaces, 3)
)
return [value];
}}
/>
<AgGridColumn
headerName={t('Amount')}
field="openVolume"
type="rightAligned"
cellRenderer={AmountCell}
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['openVolume'];
}): AmountCellProps['valueFormatted'] => {
if (!value || !data) {
return undefined;
}
cellRenderer="PriceFlashCell"
/>
</AgGrid>
);
}
);
return data;
}}
/>
<AgGridColumn
headerName={t('Mark Price')}
field="markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['markPrice'];
}) => {
if (!data) {
return undefined;
}
if (data.marketTradingMode === MarketTradingMode.OpeningAuction) {
return '-';
}
return addDecimalsFormatNumber(
value.toString(),
data.marketDecimalPlaces
);
}}
/>
<AgGridColumn
headerName={t('Entry Price')}
field="averageEntryPrice"
headerComponentParams={{
template:
'<div class="ag-cell-label-container" role="presentation">' +
` <span>${t('Liquidation price (est)')}</span>` +
' <span ref="eText" class="ag-header-cell-text"></span>' +
'</div>',
}}
flex={2}
cellRenderer="ProgressBarCell"
valueFormatter={({
data,
}: PositionsTableValueFormatterParams):
| PriceCellProps['valueFormatted']
| undefined => {
if (!data) {
return undefined;
}
const min = BigInt(data.averageEntryPrice);
const max = BigInt(data.liquidationPrice);
const mid = BigInt(data.markPrice);
const range = max - min;
return {
low: addDecimalsFormatNumber(
min.toString(),
data.marketDecimalPlaces
),
high: addDecimalsFormatNumber(
max.toString(),
data.marketDecimalPlaces
),
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
intent: data.lowMarginLevel ? Intent.Danger : undefined,
};
}}
/>
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({
value,
}: PositionsTableValueFormatterParams & {
value: Position['currentLeverage'];
}) =>
value === undefined ? undefined : formatNumber(value.toString(), 1)
}
/>
<AgGridColumn
headerName={t('Margin allocated')}
field="capitalUtilisation"
type="rightAligned"
flex={2}
cellRenderer="ProgressBarCell"
valueFormatter={({
data,
value,
}: PositionsTableValueFormatterParams & {
value: Position['capitalUtilisation'];
}): PriceCellProps['valueFormatted'] | undefined => {
if (!data) {
return undefined;
}
return {
low: `${formatNumber(value, 2)}%`,
high: addDecimalsFormatNumber(
data.totalBalance,
data.assetDecimals
),
value: Number(value),
};
}}
/>
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
cellClassRules={{
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => BigInt(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => BigInt(value) < 0,
}}
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['realisedPNL'];
}) =>
value === undefined
? undefined
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
}
cellRenderer="PriceFlashCell"
headerTooltip={t('P&L excludes any fees paid.')}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={{
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => BigInt(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => BigInt(value) < 0,
}}
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['unrealisedPNL'];
}) =>
value === undefined
? undefined
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
}
cellRenderer="PriceFlashCell"
/>
<AgGridColumn
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
valueFormatter={({
value,
}: PositionsTableValueFormatterParams & {
value: Position['updatedAt'];
}) => {
if (!value) {
return value;
}
return getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
});
export default PositionsTable;

View File

@ -0,0 +1,17 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const theme = require('../tailwindcss-config/src/theme');
const vegaCustomClasses = require('../tailwindcss-config/src/vega-custom-classes');
module.exports = {
content: [
join(__dirname, 'src/**/*.{ts,tsx,html,mdx}'),
join(__dirname, '.storybook/preview.js'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: 'class',
theme: {
extend: theme,
},
plugins: [vegaCustomClasses],
};

View File

@ -20,6 +20,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./.storybook/tsconfig.json"
}
]
}

View File

@ -16,7 +16,11 @@
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
"**/*.test.jsx",
"**/*.stories.ts",
"**/*.stories.js",
"**/*.stories.jsx",
"**/*.stories.tsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -20,6 +20,7 @@ export * from './loader';
export * from './lozenge';
export * from './popover';
export * from './price-change';
export * from './progress-bar';
export * from './radio-group';
export * from './resizable-panel';
export * from './select';

View File

@ -0,0 +1 @@
export * from './progress-bar';

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { ProgressBar } from './progress-bar';
describe('Progress Bar', () => {
it('should render successfully', () => {
const { baseElement } = render(<ProgressBar />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,57 @@
import type { Story, Meta } from '@storybook/react';
import { ProgressBar } from './progress-bar';
import { Intent } from '../../utils/intent';
export default {
component: ProgressBar,
title: 'ProgressBar',
storySort: {
order: ['Default', 'None', 'Primary', 'Danger', 'Warning', 'Success'],
},
argTypes: {
intent: {
options: Object.values(Intent).filter((x) => typeof x === 'string'),
mapping: Intent,
control: {
type: 'radio',
},
},
},
} as Meta;
const Template: Story = (args) => <ProgressBar {...args} />;
export const Default = Template.bind({});
Default.args = {
value: 10,
};
export const None = Template.bind({});
None.args = {
intent: 'None',
value: 10,
};
export const Primary = Template.bind({});
Primary.args = {
variant: 'Primary',
value: 10,
};
export const Success = Template.bind({});
Success.args = {
intent: 'Success',
value: 30,
};
export const Warning = Template.bind({});
Warning.args = {
intent: 'Warning',
value: 40,
};
export const Danger = Template.bind({});
Danger.args = {
intent: 'Danger',
value: 50,
};

View File

@ -0,0 +1,28 @@
import classNames from 'classnames';
import { getIntentBackground } from '../../utils/intent';
import { Intent } from '../../utils/intent';
interface ProgressBarProps {
value?: number;
intent?: Intent;
className?: string;
}
export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
return (
<div
style={{ height: '6px' }}
className={classNames('bg-black-10 relative', className)}
>
<div
className={classNames(
'absolute left-0 top-0 bottom-0',
intent === undefined || intent === Intent.None
? 'bg-black-60'
: getIntentBackground(intent ?? Intent.None)
)}
style={{ width: `${Math.max(0, value ?? 0)}%` }}
></div>
</div>
);
};

View File

@ -28,13 +28,25 @@ export const getIntentBorder = (intent = Intent.None) => {
};
};
export const getIntentTextAndBackground = (intent?: Intent) => {
export const getIntentBackground = (intent?: Intent) => {
return {
'bg-black text-white dark:bg-white dark:text-black': intent === Intent.None,
'bg-vega-pink text-black dark:bg-vega-yellow dark:text-black-normal':
intent === Intent.Primary,
'bg-danger text-white': intent === Intent.Danger,
'bg-warning text-black': intent === Intent.Warning,
'bg-success text-black': intent === Intent.Success,
'bg-black dark:bg-white': intent === Intent.None,
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
'bg-danger': intent === Intent.Danger,
'bg-warning': intent === Intent.Warning,
'bg-success': intent === Intent.Success,
};
};
export const getIntentText = (intent?: Intent) => {
return {
'text-white dark:text-black': intent === Intent.None,
'text-black dark:text-black-normal': intent === Intent.Primary,
'text-white': intent === Intent.Danger,
'text-black': intent === Intent.Warning || intent === Intent.Success,
};
};
export const getIntentTextAndBackground = (intent?: Intent) => {
return { ...getIntentText(intent), ...getIntentBackground(intent) };
};