feat(governance,markets,ui-toolkit): highlight successor market changes (#4449)
Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
parent
26d5a67604
commit
8d31510d5e
@ -730,6 +730,7 @@ context(
|
|||||||
.getByTestId('key-value-table-row')
|
.getByTestId('key-value-table-row')
|
||||||
.contains(heading)
|
.contains(heading)
|
||||||
.parent()
|
.parent()
|
||||||
|
.parent()
|
||||||
.siblings();
|
.siblings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,10 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
|
|||||||
|
|
||||||
export const ProposalMarketData = ({
|
export const ProposalMarketData = ({
|
||||||
marketData,
|
marketData,
|
||||||
|
parentMarketData,
|
||||||
}: {
|
}: {
|
||||||
marketData: MarketInfoWithData;
|
marketData: MarketInfoWithData;
|
||||||
|
parentMarketData?: MarketInfoWithData;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, open, close } = useMarketDataDialogStore();
|
const { isOpen, open, close } = useMarketDataDialogStore();
|
||||||
@ -58,8 +60,21 @@ export const ProposalMarketData = ({
|
|||||||
|
|
||||||
const settlementData = marketData.tradableInstrument.instrument.product
|
const settlementData = marketData.tradableInstrument.instrument.product
|
||||||
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
|
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
|
||||||
|
const parentSettlementData =
|
||||||
|
parentMarketData?.tradableInstrument.instrument?.product
|
||||||
|
?.dataSourceSpecForSettlementData?.data;
|
||||||
const terminationData = marketData.tradableInstrument.instrument.product
|
const terminationData = marketData.tradableInstrument.instrument.product
|
||||||
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
|
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
|
||||||
|
const parentTerminationData =
|
||||||
|
parentMarketData?.tradableInstrument.instrument?.product
|
||||||
|
?.dataSourceSpecForTradingTermination?.data;
|
||||||
|
|
||||||
|
const isParentSettlementDataEqual =
|
||||||
|
parentSettlementData !== undefined &&
|
||||||
|
isEqual(settlementData, parentSettlementData);
|
||||||
|
const isParentTerminationDataEqual =
|
||||||
|
parentTerminationData !== undefined &&
|
||||||
|
isEqual(terminationData, parentTerminationData);
|
||||||
|
|
||||||
const getSigners = (data: DataSourceDefinition) => {
|
const getSigners = (data: DataSourceDefinition) => {
|
||||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||||
@ -97,12 +112,22 @@ export const ProposalMarketData = ({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="key-details"
|
itemId="key-details"
|
||||||
title={t('Key details')}
|
title={t('Key details')}
|
||||||
content={<KeyDetailsInfoPanel market={marketData} />}
|
content={
|
||||||
|
<KeyDetailsInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="instrument"
|
itemId="instrument"
|
||||||
title={t('Instrument')}
|
title={t('Instrument')}
|
||||||
content={<InstrumentInfoPanel market={marketData} />}
|
content={
|
||||||
|
<InstrumentInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{isEqual(
|
{isEqual(
|
||||||
getSigners(settlementData),
|
getSigners(settlementData),
|
||||||
@ -115,6 +140,11 @@ export const ProposalMarketData = ({
|
|||||||
<OracleInfoPanel
|
<OracleInfoPanel
|
||||||
market={marketData}
|
market={marketData}
|
||||||
type="settlementData"
|
type="settlementData"
|
||||||
|
parentMarket={
|
||||||
|
isParentSettlementDataEqual
|
||||||
|
? undefined
|
||||||
|
: parentMarketData
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -127,6 +157,11 @@ export const ProposalMarketData = ({
|
|||||||
<OracleInfoPanel
|
<OracleInfoPanel
|
||||||
market={marketData}
|
market={marketData}
|
||||||
type="settlementData"
|
type="settlementData"
|
||||||
|
parentMarket={
|
||||||
|
isParentSettlementDataEqual
|
||||||
|
? undefined
|
||||||
|
: parentMarketData
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -135,11 +170,21 @@ export const ProposalMarketData = ({
|
|||||||
itemId="termination-oracle"
|
itemId="termination-oracle"
|
||||||
title={t('Termination Oracle')}
|
title={t('Termination Oracle')}
|
||||||
content={
|
content={
|
||||||
<OracleInfoPanel market={marketData} type="termination" />
|
<OracleInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
type="termination"
|
||||||
|
parentMarket={
|
||||||
|
isParentTerminationDataEqual
|
||||||
|
? undefined
|
||||||
|
: parentMarketData
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/*Note: successor markets will not differ in their settlement*/}
|
||||||
|
{/*assets, so no need to pass in parent market data for comparison.*/}
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="settlement-asset"
|
itemId="settlement-asset"
|
||||||
title={t('Settlement asset')}
|
title={t('Settlement asset')}
|
||||||
@ -148,22 +193,42 @@ export const ProposalMarketData = ({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="metadata"
|
itemId="metadata"
|
||||||
title={t('Metadata')}
|
title={t('Metadata')}
|
||||||
content={<MetadataInfoPanel market={marketData} />}
|
content={
|
||||||
|
<MetadataInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="risk-model"
|
itemId="risk-model"
|
||||||
title={t('Risk model')}
|
title={t('Risk model')}
|
||||||
content={<RiskModelInfoPanel market={marketData} />}
|
content={
|
||||||
|
<RiskModelInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="risk-parameters"
|
itemId="risk-parameters"
|
||||||
title={t('Risk parameters')}
|
title={t('Risk parameters')}
|
||||||
content={<RiskParametersInfoPanel market={marketData} />}
|
content={
|
||||||
|
<RiskParametersInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="risk-factors"
|
itemId="risk-factors"
|
||||||
title={t('Risk factors')}
|
title={t('Risk factors')}
|
||||||
content={<RiskFactorsInfoPanel market={marketData} />}
|
content={
|
||||||
|
<RiskFactorsInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{(
|
{(
|
||||||
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
||||||
@ -174,6 +239,7 @@ export const ProposalMarketData = ({
|
|||||||
content={
|
content={
|
||||||
<PriceMonitoringBoundsInfoPanel
|
<PriceMonitoringBoundsInfoPanel
|
||||||
market={marketData}
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
triggerIndex={triggerIndex}
|
triggerIndex={triggerIndex}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -183,13 +249,21 @@ export const ProposalMarketData = ({
|
|||||||
itemId="liqudity-monitoring-parameters"
|
itemId="liqudity-monitoring-parameters"
|
||||||
title={t('Liquidity monitoring parameters')}
|
title={t('Liquidity monitoring parameters')}
|
||||||
content={
|
content={
|
||||||
<LiquidityMonitoringParametersInfoPanel market={marketData} />
|
<LiquidityMonitoringParametersInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="liquidity-price-range"
|
itemId="liquidity-price-range"
|
||||||
title={t('Liquidity price range')}
|
title={t('Liquidity price range')}
|
||||||
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
|
content={
|
||||||
|
<LiquidityPriceRangeInfoPanel
|
||||||
|
market={marketData}
|
||||||
|
parentMarket={parentMarketData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,7 @@ export interface ProposalProps {
|
|||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
networkParams: Partial<NetworkParamsResult>;
|
networkParams: Partial<NetworkParamsResult>;
|
||||||
newMarketData?: MarketInfoWithData | null;
|
newMarketData?: MarketInfoWithData | null;
|
||||||
|
parentMarketData?: MarketInfoWithData | null;
|
||||||
assetData?: AssetQuery | null;
|
assetData?: AssetQuery | null;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
restData: any;
|
restData: any;
|
||||||
@ -46,6 +47,7 @@ export const Proposal = ({
|
|||||||
networkParams,
|
networkParams,
|
||||||
restData,
|
restData,
|
||||||
newMarketData,
|
newMarketData,
|
||||||
|
parentMarketData,
|
||||||
assetData,
|
assetData,
|
||||||
originalMarketProposalRestData,
|
originalMarketProposalRestData,
|
||||||
mostRecentlyEnactedAssociatedMarketProposal,
|
mostRecentlyEnactedAssociatedMarketProposal,
|
||||||
@ -157,7 +159,10 @@ export const Proposal = ({
|
|||||||
|
|
||||||
{newMarketData && (
|
{newMarketData && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<ProposalMarketData marketData={newMarketData} />
|
<ProposalMarketData
|
||||||
|
marketData={newMarketData}
|
||||||
|
parentMarketData={parentMarketData ? parentMarketData : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -14,6 +14,9 @@ import {
|
|||||||
NetworkParams,
|
NetworkParams,
|
||||||
useNetworkParams,
|
useNetworkParams,
|
||||||
} from '@vegaprotocol/network-parameters';
|
} from '@vegaprotocol/network-parameters';
|
||||||
|
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
|
||||||
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||||
|
|
||||||
export const ProposalContainer = () => {
|
export const ProposalContainer = () => {
|
||||||
const [
|
const [
|
||||||
@ -54,6 +57,10 @@ export const ProposalContainer = () => {
|
|||||||
skip: !params.proposalId,
|
skip: !params.proposalId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successor = useSuccessorMarketProposalDetails(params.proposalId);
|
||||||
|
|
||||||
|
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: {
|
state: {
|
||||||
data: originalMarketProposalRestData,
|
data: originalMarketProposalRestData,
|
||||||
@ -96,6 +103,36 @@ export const ProposalContainer = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: parentMarketId,
|
||||||
|
loading: parentMarketIdLoading,
|
||||||
|
error: parentMarketIdError,
|
||||||
|
} = useParentMarketIdQuery({
|
||||||
|
variables: {
|
||||||
|
marketId: newMarketData?.data?.market?.id || '',
|
||||||
|
},
|
||||||
|
skip:
|
||||||
|
!FLAGS.SUCCESSOR_MARKETS ||
|
||||||
|
!isSuccessor ||
|
||||||
|
!newMarketData?.data?.market?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: parentMarketData,
|
||||||
|
loading: parentMarketLoading,
|
||||||
|
error: parentMarketError,
|
||||||
|
} = useDataProvider({
|
||||||
|
dataProvider: marketInfoWithDataProvider,
|
||||||
|
skipUpdates: true,
|
||||||
|
variables: {
|
||||||
|
marketId: parentMarketId?.market?.parentMarketID || '',
|
||||||
|
skip:
|
||||||
|
!FLAGS.SUCCESSOR_MARKETS ||
|
||||||
|
!isSuccessor ||
|
||||||
|
!parentMarketId?.market?.parentMarketID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: assetData,
|
data: assetData,
|
||||||
loading: assetLoading,
|
loading: assetLoading,
|
||||||
@ -160,6 +197,8 @@ export const ProposalContainer = () => {
|
|||||||
newMarketLoading ||
|
newMarketLoading ||
|
||||||
assetLoading ||
|
assetLoading ||
|
||||||
networkParamsLoading ||
|
networkParamsLoading ||
|
||||||
|
parentMarketIdLoading ||
|
||||||
|
parentMarketLoading ||
|
||||||
(restLoading ? (restLoading as boolean) : false) ||
|
(restLoading ? (restLoading as boolean) : false) ||
|
||||||
(originalMarketProposalRestLoading
|
(originalMarketProposalRestLoading
|
||||||
? (originalMarketProposalRestLoading as boolean)
|
? (originalMarketProposalRestLoading as boolean)
|
||||||
@ -172,15 +211,18 @@ export const ProposalContainer = () => {
|
|||||||
error ||
|
error ||
|
||||||
newMarketError ||
|
newMarketError ||
|
||||||
assetError ||
|
assetError ||
|
||||||
|
networkParamsError ||
|
||||||
|
parentMarketIdError ||
|
||||||
|
parentMarketError ||
|
||||||
restError ||
|
restError ||
|
||||||
originalMarketProposalRestError ||
|
originalMarketProposalRestError ||
|
||||||
previouslyEnactedMarketProposalsRestError ||
|
previouslyEnactedMarketProposalsRestError
|
||||||
networkParamsError
|
|
||||||
}
|
}
|
||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
...networkParams,
|
...networkParams,
|
||||||
...(newMarketData ? { newMarketData } : {}),
|
...(newMarketData ? { newMarketData } : {}),
|
||||||
|
...(parentMarketData ? { parentMarketData } : {}),
|
||||||
...(assetData ? { assetData } : {}),
|
...(assetData ? { assetData } : {}),
|
||||||
...(restData ? { restData } : {}),
|
...(restData ? { restData } : {}),
|
||||||
...(originalMarketProposalRestData
|
...(originalMarketProposalRestData
|
||||||
@ -197,6 +239,7 @@ export const ProposalContainer = () => {
|
|||||||
networkParams={networkParams}
|
networkParams={networkParams}
|
||||||
restData={restData}
|
restData={restData}
|
||||||
newMarketData={newMarketData}
|
newMarketData={newMarketData}
|
||||||
|
parentMarketData={parentMarketData}
|
||||||
assetData={assetData}
|
assetData={assetData}
|
||||||
originalMarketProposalRestData={originalMarketProposalRestData}
|
originalMarketProposalRestData={originalMarketProposalRestData}
|
||||||
mostRecentlyEnactedAssociatedMarketProposal={
|
mostRecentlyEnactedAssociatedMarketProposal={
|
||||||
|
@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider<
|
|||||||
getData,
|
getData,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAssetDataProvider = (assetId: string) => {
|
export const useAssetDataProvider = (assetId: string, skip?: boolean) => {
|
||||||
return useDataProvider({
|
return useDataProvider({
|
||||||
dataProvider: assetProvider,
|
dataProvider: assetProvider,
|
||||||
variables: { assetId: assetId || '' },
|
variables: { assetId: assetId || '' },
|
||||||
skip: !assetId,
|
skip: !assetId || skip,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,10 @@ import {
|
|||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
|
Intent,
|
||||||
KeyValueTable,
|
KeyValueTable,
|
||||||
KeyValueTableRow,
|
KeyValueTableRow,
|
||||||
|
Lozenge,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
@ -22,9 +24,11 @@ interface RowProps {
|
|||||||
unformatted?: boolean;
|
unformatted?: boolean;
|
||||||
assetSymbol?: string;
|
assetSymbol?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
parentValue?: ReactNode;
|
||||||
|
hasParentData?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({
|
export const Row = ({
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
decimalPlaces,
|
decimalPlaces,
|
||||||
@ -32,7 +36,14 @@ const Row = ({
|
|||||||
unformatted,
|
unformatted,
|
||||||
assetSymbol = '',
|
assetSymbol = '',
|
||||||
noBorder = true,
|
noBorder = true,
|
||||||
|
parentValue,
|
||||||
|
hasParentData,
|
||||||
}: RowProps) => {
|
}: RowProps) => {
|
||||||
|
// Note: we need both 'parentValue' and 'hasParentData' to do a conditional
|
||||||
|
// check to differentiate between when parentData itself is missing and when
|
||||||
|
// a specific parentValue is missing. These values are only used when we
|
||||||
|
// have successor market parent data.
|
||||||
|
|
||||||
const className = 'text-sm';
|
const className = 'text-sm';
|
||||||
|
|
||||||
const getFormattedValue = (value: ReactNode) => {
|
const getFormattedValue = (value: ReactNode) => {
|
||||||
@ -55,6 +66,10 @@ const Row = ({
|
|||||||
const formattedValue = getFormattedValue(value);
|
const formattedValue = getFormattedValue(value);
|
||||||
|
|
||||||
if (!formattedValue) return null;
|
if (!formattedValue) return null;
|
||||||
|
|
||||||
|
const newValueInSuccessorMarket = hasParentData && value && !parentValue;
|
||||||
|
const valueDiffersFromParentMarket = parentValue && parentValue !== value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyValueTableRow
|
<KeyValueTableRow
|
||||||
key={field}
|
key={field}
|
||||||
@ -63,10 +78,35 @@ const Row = ({
|
|||||||
dtClassName={className}
|
dtClassName={className}
|
||||||
ddClassName={className}
|
ddClassName={className}
|
||||||
>
|
>
|
||||||
<Tooltip description={tooltipMapping[field]} align="start">
|
<div className="flex items-center gap-3">
|
||||||
<div tabIndex={-1}>{startCase(t(field))}</div>
|
<Tooltip description={tooltipMapping[field]} align="start">
|
||||||
</Tooltip>
|
<div tabIndex={-1}>{startCase(t(field))}</div>
|
||||||
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
|
</Tooltip>
|
||||||
|
|
||||||
|
{valueDiffersFromParentMarket && (
|
||||||
|
<Lozenge className="py-0" variant={Intent.Primary}>
|
||||||
|
{t('Updated')}
|
||||||
|
</Lozenge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newValueInSuccessorMarket && (
|
||||||
|
<Lozenge className="py-0" variant={Intent.Primary}>
|
||||||
|
{t('Added')}
|
||||||
|
</Lozenge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ wordBreak: 'break-word' }}>
|
||||||
|
{valueDiffersFromParentMarket ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="line-through">
|
||||||
|
{getFormattedValue(parentValue)}
|
||||||
|
</span>
|
||||||
|
<span>{formattedValue}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
formattedValue
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</KeyValueTableRow>
|
</KeyValueTableRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -79,6 +119,7 @@ export interface MarketInfoTableProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
assetSymbol?: string;
|
assetSymbol?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
parentData?: Record<string, ReactNode> | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarketInfoTable = ({
|
export const MarketInfoTable = ({
|
||||||
@ -89,25 +130,33 @@ export const MarketInfoTable = ({
|
|||||||
children,
|
children,
|
||||||
assetSymbol,
|
assetSymbol,
|
||||||
noBorder,
|
noBorder,
|
||||||
|
parentData,
|
||||||
}: MarketInfoTableProps) => {
|
}: MarketInfoTableProps) => {
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasParentData = parentData !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KeyValueTable>
|
<KeyValueTable>
|
||||||
{Object.entries(data).map(([key, value]) => (
|
<>
|
||||||
<Row
|
{Object.entries(data).map(([key, value]) => (
|
||||||
key={key}
|
<Row
|
||||||
field={key}
|
key={key}
|
||||||
value={value}
|
field={key}
|
||||||
decimalPlaces={decimalPlaces}
|
value={value}
|
||||||
assetSymbol={assetSymbol}
|
decimalPlaces={decimalPlaces}
|
||||||
asPercentage={asPercentage}
|
assetSymbol={assetSymbol}
|
||||||
unformatted={unformatted}
|
asPercentage={asPercentage}
|
||||||
noBorder={noBorder}
|
unformatted={unformatted}
|
||||||
/>
|
noBorder={noBorder}
|
||||||
))}
|
parentValue={parentData?.[key]}
|
||||||
|
hasParentData={hasParentData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
</KeyValueTable>
|
</KeyValueTable>
|
||||||
<div className="flex flex-col gap-2">{children}</div>
|
<div className="flex flex-col gap-2">{children}</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -47,11 +48,14 @@ import {
|
|||||||
useSuccessorMarketQuery,
|
useSuccessorMarketQuery,
|
||||||
} from '../../__generated__';
|
} from '../../__generated__';
|
||||||
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
|
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
|
||||||
|
import type { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
import type { Signer } from '@vegaprotocol/types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
|
|
||||||
type MarketInfoProps = {
|
type MarketInfoProps = {
|
||||||
market: MarketInfo;
|
market: MarketInfo;
|
||||||
|
parentMarket?: MarketInfo;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -156,21 +160,43 @@ export const InsurancePoolInfoPanel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
export const KeyDetailsInfoPanel = ({
|
||||||
const { data: parentData } = useParentMarketIdQuery({
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => {
|
||||||
|
const { data: parentMarketIdData } = useParentMarketIdQuery({
|
||||||
variables: {
|
variables: {
|
||||||
marketId: market.id,
|
marketId: market.id,
|
||||||
},
|
},
|
||||||
skip: !FLAGS.SUCCESSOR_MARKETS,
|
skip: !FLAGS.SUCCESSOR_MARKETS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: successor } = useSuccessorMarketProposalDetailsQuery({
|
const { data: successorProposalDetails } =
|
||||||
|
useSuccessorMarketProposalDetailsQuery({
|
||||||
|
variables: {
|
||||||
|
proposalId: market.proposal?.id || '',
|
||||||
|
},
|
||||||
|
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The following queries are needed as the parent market could also have been a successor market.
|
||||||
|
// Note: the parent market is only passed to this component if the successor markets flag is enabled,
|
||||||
|
// so that check is not needed in the skip.
|
||||||
|
const { data: grandparentMarketIdData } = useParentMarketIdQuery({
|
||||||
variables: {
|
variables: {
|
||||||
proposalId: market.proposal?.id || '',
|
marketId: parentMarket?.id || '',
|
||||||
},
|
},
|
||||||
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
|
skip: !parentMarket?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: parentSuccessorProposalDetails } =
|
||||||
|
useSuccessorMarketProposalDetailsQuery({
|
||||||
|
variables: {
|
||||||
|
proposalId: parentMarket?.proposal?.id || '',
|
||||||
|
},
|
||||||
|
skip: !parentMarket?.proposal?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const assetDecimals =
|
const assetDecimals =
|
||||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||||
|
|
||||||
@ -181,11 +207,12 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
? {
|
? {
|
||||||
name: market.tradableInstrument.instrument.name,
|
name: market.tradableInstrument.instrument.name,
|
||||||
marketID: market.id,
|
marketID: market.id,
|
||||||
parentMarketID: parentData?.market?.parentMarketID || '-',
|
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
|
||||||
insurancePoolFraction:
|
insurancePoolFraction:
|
||||||
(successor?.proposal?.terms.change.__typename === 'NewMarket' &&
|
(successorProposalDetails?.proposal?.terms.change.__typename ===
|
||||||
successor.proposal.terms.change.successorConfiguration
|
'NewMarket' &&
|
||||||
?.insurancePoolFraction) ||
|
successorProposalDetails.proposal.terms.change
|
||||||
|
.successorConfiguration?.insurancePoolFraction) ||
|
||||||
'-',
|
'-',
|
||||||
tradingMode:
|
tradingMode:
|
||||||
market.tradingMode &&
|
market.tradingMode &&
|
||||||
@ -205,6 +232,28 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
settlementAssetDecimalPlaces: assetDecimals,
|
settlementAssetDecimalPlaces: assetDecimals,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parentData={
|
||||||
|
parentMarket && {
|
||||||
|
name: parentMarket?.tradableInstrument?.instrument?.name,
|
||||||
|
marketID: parentMarket?.id,
|
||||||
|
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
|
||||||
|
insurancePoolFraction:
|
||||||
|
parentSuccessorProposalDetails?.proposal?.terms.change
|
||||||
|
.__typename === 'NewMarket' &&
|
||||||
|
parentSuccessorProposalDetails.proposal.terms.change
|
||||||
|
.successorConfiguration?.insurancePoolFraction,
|
||||||
|
tradingMode:
|
||||||
|
parentMarket?.tradingMode &&
|
||||||
|
MarketTradingModeMapping[
|
||||||
|
parentMarket.tradingMode as MarketTradingMode
|
||||||
|
],
|
||||||
|
marketDecimalPlaces: parentMarket?.decimalPlaces,
|
||||||
|
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
|
||||||
|
settlementAssetDecimalPlaces:
|
||||||
|
parentMarket?.tradableInstrument?.instrument?.product
|
||||||
|
?.settlementAsset?.decimals,
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -329,7 +378,10 @@ export const SuccessionLineInfoPanel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
|
export const InstrumentInfoPanel = ({
|
||||||
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => (
|
||||||
<MarketInfoTable
|
<MarketInfoTable
|
||||||
data={{
|
data={{
|
||||||
marketName: market.tradableInstrument.instrument.name,
|
marketName: market.tradableInstrument.instrument.name,
|
||||||
@ -337,6 +389,16 @@ export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
|
|||||||
productType: market.tradableInstrument.instrument.product.__typename,
|
productType: market.tradableInstrument.instrument.product.__typename,
|
||||||
quoteName: market.tradableInstrument.instrument.product.quoteName,
|
quoteName: market.tradableInstrument.instrument.product.quoteName,
|
||||||
}}
|
}}
|
||||||
|
parentData={
|
||||||
|
parentMarket && {
|
||||||
|
marketName: parentMarket?.tradableInstrument?.instrument?.name,
|
||||||
|
code: parentMarket?.tradableInstrument?.instrument?.code,
|
||||||
|
productType:
|
||||||
|
parentMarket?.tradableInstrument?.instrument?.product?.__typename,
|
||||||
|
quoteName:
|
||||||
|
parentMarket?.tradableInstrument?.instrument?.product?.quoteName,
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -349,6 +411,7 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||||
[market]
|
[market]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||||
return asset ? (
|
return asset ? (
|
||||||
<>
|
<>
|
||||||
@ -371,54 +434,148 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MetadataInfoPanel = ({ market }: MarketInfoProps) => (
|
const getMarketMetadata = (market: MarketInfo) =>
|
||||||
|
market.tradableInstrument.instrument.metadata.tags
|
||||||
|
?.map((tag) => {
|
||||||
|
const [key, value] = tag.split(':');
|
||||||
|
return { [key]: value };
|
||||||
|
})
|
||||||
|
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
||||||
|
|
||||||
|
export const MetadataInfoPanel = ({
|
||||||
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => (
|
||||||
<MarketInfoTable
|
<MarketInfoTable
|
||||||
data={{
|
data={{
|
||||||
expiryDate: getMarketExpiryDateFormatted(
|
expiryDate: getMarketExpiryDateFormatted(
|
||||||
market.tradableInstrument.instrument.metadata.tags
|
market.tradableInstrument.instrument.metadata.tags
|
||||||
),
|
),
|
||||||
...market.tradableInstrument.instrument.metadata.tags
|
...(getMarketMetadata(market) || {}),
|
||||||
?.map((tag) => {
|
|
||||||
const [key, value] = tag.split(':');
|
|
||||||
return { [key]: value };
|
|
||||||
})
|
|
||||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
|
||||||
}}
|
}}
|
||||||
|
parentData={
|
||||||
|
parentMarket && {
|
||||||
|
expiryDate: getMarketExpiryDateFormatted(
|
||||||
|
parentMarket.tradableInstrument.instrument.metadata.tags
|
||||||
|
),
|
||||||
|
...(getMarketMetadata(parentMarket) || {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => {
|
export const RiskModelInfoPanel = ({
|
||||||
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => {
|
||||||
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
|
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
|
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
|
||||||
return <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
|
|
||||||
|
let parentData;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentMarket?.tradableInstrument?.riskModel?.__typename ===
|
||||||
|
'LogNormalRiskModel'
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
tau: parentTau,
|
||||||
|
riskAversionParameter: parentRiskAversionParameter,
|
||||||
|
} = market.tradableInstrument.riskModel;
|
||||||
|
|
||||||
|
parentData = {
|
||||||
|
tau: parentTau,
|
||||||
|
riskAversionParameter: parentRiskAversionParameter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarketInfoTable
|
||||||
|
data={{ tau, riskAversionParameter }}
|
||||||
|
parentData={parentData}
|
||||||
|
unformatted
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => {
|
export const RiskParametersInfoPanel = ({
|
||||||
if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') {
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => {
|
||||||
|
const marketType = market.tradableInstrument.riskModel.__typename;
|
||||||
|
|
||||||
|
let data, parentData;
|
||||||
|
|
||||||
|
if (marketType === 'LogNormalRiskModel') {
|
||||||
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
|
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
|
||||||
return <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
|
data = { r, sigma, mu };
|
||||||
}
|
|
||||||
if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') {
|
if (
|
||||||
|
parentMarket?.tradableInstrument?.riskModel.__typename ===
|
||||||
|
'LogNormalRiskModel'
|
||||||
|
) {
|
||||||
|
const parentParams = parentMarket.tradableInstrument.riskModel.params;
|
||||||
|
parentData = {
|
||||||
|
r: parentParams.r,
|
||||||
|
sigma: parentParams.sigma,
|
||||||
|
mu: parentParams.mu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (marketType === 'SimpleRiskModel') {
|
||||||
const { factorLong, factorShort } =
|
const { factorLong, factorShort } =
|
||||||
market.tradableInstrument.riskModel.params;
|
market.tradableInstrument.riskModel.params;
|
||||||
return <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
|
data = { factorLong, factorShort };
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentMarket?.tradableInstrument?.riskModel.__typename ===
|
||||||
|
'SimpleRiskModel'
|
||||||
|
) {
|
||||||
|
const parentParams = parentMarket.tradableInstrument.riskModel.params;
|
||||||
|
parentData = {
|
||||||
|
factorLong: parentParams.factorLong,
|
||||||
|
factorShort: parentParams.factorShort,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
|
export const RiskFactorsInfoPanel = ({
|
||||||
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => {
|
||||||
if (!market.riskFactors) {
|
if (!market.riskFactors) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { short, long } = market.riskFactors;
|
const { short, long } = market.riskFactors;
|
||||||
return <MarketInfoTable data={{ short, long }} unformatted />;
|
|
||||||
|
let parentData;
|
||||||
|
|
||||||
|
if (parentMarket?.riskFactors) {
|
||||||
|
const parentShort = parentMarket.riskFactors.short;
|
||||||
|
const parentLong = parentMarket.riskFactors.long;
|
||||||
|
parentData = { short: parentShort, long: parentLong };
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarketInfoTable
|
||||||
|
data={{ short, long }}
|
||||||
|
parentData={parentData}
|
||||||
|
unformatted
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriceMonitoringBoundsInfoPanel = ({
|
export const PriceMonitoringBoundsInfoPanel = ({
|
||||||
market,
|
market,
|
||||||
triggerIndex,
|
triggerIndex,
|
||||||
|
parentMarket,
|
||||||
}: MarketInfoProps & {
|
}: MarketInfoProps & {
|
||||||
triggerIndex: number;
|
triggerIndex: number;
|
||||||
}) => {
|
}) => {
|
||||||
@ -426,11 +583,35 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
|||||||
dataProvider: marketDataProvider,
|
dataProvider: marketDataProvider,
|
||||||
variables: { marketId: market.id },
|
variables: { marketId: market.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: parentData } = useDataProvider({
|
||||||
|
dataProvider: marketDataProvider,
|
||||||
|
variables: { marketId: parentMarket?.id || '' },
|
||||||
|
skip:
|
||||||
|
!parentMarket ||
|
||||||
|
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
|
||||||
|
triggerIndex
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const quoteUnit =
|
const quoteUnit =
|
||||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||||
|
const parentQuoteUnit =
|
||||||
|
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
|
||||||
|
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
|
||||||
|
|
||||||
const trigger =
|
const trigger =
|
||||||
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||||
|
const parentTrigger =
|
||||||
|
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||||
|
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
|
||||||
|
|
||||||
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
|
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
|
||||||
|
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
|
||||||
|
|
||||||
|
const shouldShowParentData =
|
||||||
|
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
|
||||||
|
|
||||||
if (!trigger) {
|
if (!trigger) {
|
||||||
console.error(
|
console.error(
|
||||||
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
|
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
|
||||||
@ -457,6 +638,14 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
|||||||
highestPrice: bounds.maxValidPrice,
|
highestPrice: bounds.maxValidPrice,
|
||||||
lowestPrice: bounds.minValidPrice,
|
lowestPrice: bounds.minValidPrice,
|
||||||
}}
|
}}
|
||||||
|
parentData={
|
||||||
|
shouldShowParentData
|
||||||
|
? {
|
||||||
|
highestPrice: parentBounds.maxValidPrice,
|
||||||
|
lowestPrice: parentBounds.minValidPrice,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
decimalPlaces={market.decimalPlaces}
|
decimalPlaces={market.decimalPlaces}
|
||||||
assetSymbol={quoteUnit}
|
assetSymbol={quoteUnit}
|
||||||
/>
|
/>
|
||||||
@ -472,18 +661,31 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
|||||||
|
|
||||||
export const LiquidityMonitoringParametersInfoPanel = ({
|
export const LiquidityMonitoringParametersInfoPanel = ({
|
||||||
market,
|
market,
|
||||||
}: MarketInfoProps) => (
|
parentMarket,
|
||||||
<MarketInfoTable
|
}: MarketInfoProps) => {
|
||||||
data={{
|
const marketData = {
|
||||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||||
timeWindow:
|
timeWindow:
|
||||||
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
|
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
|
||||||
scalingFactor:
|
scalingFactor:
|
||||||
market.liquidityMonitoringParameters.targetStakeParameters
|
market.liquidityMonitoringParameters.targetStakeParameters.scalingFactor,
|
||||||
.scalingFactor,
|
};
|
||||||
}}
|
|
||||||
/>
|
const parentMarketData = parentMarket
|
||||||
);
|
? {
|
||||||
|
triggeringRatio:
|
||||||
|
parentMarket.liquidityMonitoringParameters.triggeringRatio,
|
||||||
|
timeWindow:
|
||||||
|
parentMarket.liquidityMonitoringParameters.targetStakeParameters
|
||||||
|
.timeWindow,
|
||||||
|
scalingFactor:
|
||||||
|
parentMarket.liquidityMonitoringParameters.targetStakeParameters
|
||||||
|
.scalingFactor,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return <MarketInfoTable data={marketData} parentData={parentMarketData} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
||||||
const assetDecimals =
|
const assetDecimals =
|
||||||
@ -510,16 +712,61 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
export const LiquidityPriceRangeInfoPanel = ({
|
||||||
|
market,
|
||||||
|
parentMarket,
|
||||||
|
}: MarketInfoProps) => {
|
||||||
const quoteUnit =
|
const quoteUnit =
|
||||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||||
|
const parentQuoteUnit =
|
||||||
|
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
|
||||||
|
|
||||||
const liquidityPriceRange = formatNumberPercentage(
|
const liquidityPriceRange = formatNumberPercentage(
|
||||||
new BigNumber(market.lpPriceRange).times(100)
|
new BigNumber(market.lpPriceRange).times(100)
|
||||||
);
|
);
|
||||||
|
const parentLiquidityPriceRange = parentMarket
|
||||||
|
? formatNumberPercentage(
|
||||||
|
new BigNumber(parentMarket.lpPriceRange).times(100)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
const { data } = useDataProvider({
|
const { data } = useDataProvider({
|
||||||
dataProvider: marketDataProvider,
|
dataProvider: marketDataProvider,
|
||||||
variables: { marketId: market.id },
|
variables: { marketId: market.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: parentMarketData } = useDataProvider({
|
||||||
|
dataProvider: marketDataProvider,
|
||||||
|
variables: { marketId: parentMarket?.id || '' },
|
||||||
|
skip: !parentMarket,
|
||||||
|
});
|
||||||
|
|
||||||
|
let parentData;
|
||||||
|
|
||||||
|
if (parentMarket && parentMarketData && quoteUnit === parentQuoteUnit) {
|
||||||
|
parentData = {
|
||||||
|
liquidityPriceRange: `${parentLiquidityPriceRange} of mid price`,
|
||||||
|
lowestPrice:
|
||||||
|
parentMarketData?.midPrice &&
|
||||||
|
`${addDecimalsFormatNumber(
|
||||||
|
new BigNumber(1)
|
||||||
|
.minus(parentMarket.lpPriceRange)
|
||||||
|
.times(parentMarketData.midPrice)
|
||||||
|
.toString(),
|
||||||
|
parentMarket.decimalPlaces
|
||||||
|
)} ${quoteUnit}`,
|
||||||
|
highestPrice:
|
||||||
|
parentMarketData?.midPrice &&
|
||||||
|
`${addDecimalsFormatNumber(
|
||||||
|
new BigNumber(1)
|
||||||
|
.plus(parentMarket.lpPriceRange)
|
||||||
|
.times(parentMarketData.midPrice)
|
||||||
|
.toString(),
|
||||||
|
parentMarket.decimalPlaces
|
||||||
|
)} ${quoteUnit}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm mb-2">
|
<p className="text-sm mb-2">
|
||||||
@ -552,6 +799,7 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
market.decimalPlaces
|
market.decimalPlaces
|
||||||
)} ${quoteUnit}`,
|
)} ${quoteUnit}`,
|
||||||
}}
|
}}
|
||||||
|
parentData={parentData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -560,8 +808,12 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
export const OracleInfoPanel = ({
|
export const OracleInfoPanel = ({
|
||||||
market,
|
market,
|
||||||
type,
|
type,
|
||||||
|
parentMarket,
|
||||||
}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => {
|
}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => {
|
||||||
|
// If this is a successor market, this component will only receive parent market
|
||||||
|
// data if the termination or settlement data is different from the parent.
|
||||||
const product = market.tradableInstrument.instrument.product;
|
const product = market.tradableInstrument.instrument.product;
|
||||||
|
const parentProduct = parentMarket?.tradableInstrument?.instrument?.product;
|
||||||
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
|
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
|
||||||
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
|
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
|
||||||
|
|
||||||
@ -570,12 +822,33 @@ export const OracleInfoPanel = ({
|
|||||||
? product.dataSourceSpecForSettlementData.id
|
? product.dataSourceSpecForSettlementData.id
|
||||||
: product.dataSourceSpecForTradingTermination.id;
|
: product.dataSourceSpecForTradingTermination.id;
|
||||||
|
|
||||||
|
const parentDataSourceSpecId =
|
||||||
|
type === 'settlementData'
|
||||||
|
? parentProduct?.dataSourceSpecForSettlementData?.id
|
||||||
|
: parentProduct?.dataSourceSpecForTradingTermination?.id;
|
||||||
|
|
||||||
const dataSourceSpec = (
|
const dataSourceSpec = (
|
||||||
type === 'settlementData'
|
type === 'settlementData'
|
||||||
? product.dataSourceSpecForSettlementData.data
|
? product.dataSourceSpecForSettlementData.data
|
||||||
: product.dataSourceSpecForTradingTermination.data
|
: product.dataSourceSpecForTradingTermination.data
|
||||||
) as DataSourceDefinition;
|
) as DataSourceDefinition;
|
||||||
|
|
||||||
|
const parentDataSourceSpec =
|
||||||
|
type === 'settlementData'
|
||||||
|
? parentProduct?.dataSourceSpecForSettlementData?.data
|
||||||
|
: (parentProduct?.dataSourceSpecForTradingTermination
|
||||||
|
?.data as DataSourceDefinition);
|
||||||
|
|
||||||
|
const isParentDataSourceSpecEqual =
|
||||||
|
parentDataSourceSpec !== undefined &&
|
||||||
|
dataSourceSpec === parentDataSourceSpec;
|
||||||
|
const isParentDataSourceSpecIdEqual =
|
||||||
|
parentDataSourceSpecId !== undefined &&
|
||||||
|
dataSourceSpecId === parentDataSourceSpecId;
|
||||||
|
|
||||||
|
// We'll only provide successor parent data (if it differs) to the
|
||||||
|
// DataSourceProof component. Having an old external link struck through
|
||||||
|
// is unlikely to be useful.
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<DataSourceProof
|
<DataSourceProof
|
||||||
@ -584,7 +857,14 @@ export const OracleInfoPanel = ({
|
|||||||
providers={data}
|
providers={data}
|
||||||
type={type}
|
type={type}
|
||||||
dataSourceSpecId={dataSourceSpecId}
|
dataSourceSpecId={dataSourceSpecId}
|
||||||
|
parentData={
|
||||||
|
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
|
||||||
|
}
|
||||||
|
parentDataSourceSpecId={
|
||||||
|
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
data-testid="oracle-spec-links"
|
data-testid="oracle-spec-links"
|
||||||
href={`${VEGA_EXPLORER_URL}/oracles/${
|
href={`${VEGA_EXPLORER_URL}/oracles/${
|
||||||
@ -606,14 +886,28 @@ export const DataSourceProof = ({
|
|||||||
providers,
|
providers,
|
||||||
type,
|
type,
|
||||||
dataSourceSpecId,
|
dataSourceSpecId,
|
||||||
|
parentData,
|
||||||
|
parentDataSourceSpecId,
|
||||||
}: {
|
}: {
|
||||||
data: DataSourceDefinition;
|
data: DataSourceDefinition;
|
||||||
providers: Provider[] | undefined;
|
providers: Provider[] | undefined;
|
||||||
type: 'settlementData' | 'termination';
|
type: 'settlementData' | 'termination';
|
||||||
dataSourceSpecId: string;
|
dataSourceSpecId: string;
|
||||||
|
parentData?: DataSourceDefinition;
|
||||||
|
parentDataSourceSpecId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
// If this is a successor market, we'll only pass parent data to child
|
||||||
|
// components for comparison if the data differs from the parent market.
|
||||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||||
const signers = data.sourceType.sourceType.signers || [];
|
const signers = data.sourceType.sourceType.signers || [];
|
||||||
|
let parentSigners: Signer[];
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentData &&
|
||||||
|
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
|
||||||
|
) {
|
||||||
|
parentSigners = parentData.sourceType.sourceType?.signers || [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!providers?.length) {
|
if (!providers?.length) {
|
||||||
return <NoOracleProof type={type} />;
|
return <NoOracleProof type={type} />;
|
||||||
@ -622,7 +916,14 @@ export const DataSourceProof = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{signers.map(({ signer }, i) => {
|
{signers.map(({ signer }, i) => {
|
||||||
return (
|
const parentSigner = parentSigners?.find(
|
||||||
|
({ signer: ParentSigner }) =>
|
||||||
|
ParentSigner.__typename === signer.__typename
|
||||||
|
)?.signer;
|
||||||
|
|
||||||
|
const isParentSignerEqual = isEqual(signer, parentSigner);
|
||||||
|
|
||||||
|
return isParentSignerEqual ? (
|
||||||
<OracleLink
|
<OracleLink
|
||||||
key={i}
|
key={i}
|
||||||
providers={providers}
|
providers={providers}
|
||||||
@ -630,6 +931,16 @@ export const DataSourceProof = ({
|
|||||||
type={type}
|
type={type}
|
||||||
dataSourceSpecId={dataSourceSpecId}
|
dataSourceSpecId={dataSourceSpecId}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<OracleLink
|
||||||
|
key={i}
|
||||||
|
providers={providers}
|
||||||
|
signer={signer}
|
||||||
|
type={type}
|
||||||
|
dataSourceSpecId={dataSourceSpecId}
|
||||||
|
parentSigner={parentSigner}
|
||||||
|
parentDataSourceSpecId={parentDataSourceSpecId}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -663,18 +974,8 @@ export const DataSourceProof = ({
|
|||||||
return <div>{t('Invalid data source')}</div>;
|
return <div>{t('Invalid data source')}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OracleLink = ({
|
const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
|
||||||
providers,
|
providers.filter((p) => {
|
||||||
signer,
|
|
||||||
type,
|
|
||||||
dataSourceSpecId,
|
|
||||||
}: {
|
|
||||||
providers: Provider[];
|
|
||||||
signer: SignerKind;
|
|
||||||
type: 'settlementData' | 'termination';
|
|
||||||
dataSourceSpecId: string;
|
|
||||||
}) => {
|
|
||||||
const signerProviders = providers.filter((p) => {
|
|
||||||
if (signer.__typename === 'PubKey') {
|
if (signer.__typename === 'PubKey') {
|
||||||
if (
|
if (
|
||||||
p.oracle.type === 'public_key' &&
|
p.oracle.type === 'public_key' &&
|
||||||
@ -696,19 +997,62 @@ const OracleLink = ({
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const OracleLink = ({
|
||||||
|
providers,
|
||||||
|
signer,
|
||||||
|
type,
|
||||||
|
dataSourceSpecId,
|
||||||
|
parentSigner,
|
||||||
|
parentDataSourceSpecId,
|
||||||
|
}: {
|
||||||
|
providers: Provider[];
|
||||||
|
signer: SignerKind;
|
||||||
|
type: 'settlementData' | 'termination';
|
||||||
|
dataSourceSpecId: string;
|
||||||
|
parentSigner?: SignerKind;
|
||||||
|
parentDataSourceSpecId?: string;
|
||||||
|
}) => {
|
||||||
|
// If this is a successor market, the parent market data will only have been passed
|
||||||
|
// in if it differs from the current data.
|
||||||
|
const signerProviders = getSignerProviders(signer, providers);
|
||||||
|
const parentSignerProviders = parentSigner
|
||||||
|
? getSignerProviders(parentSigner, providers)
|
||||||
|
: [];
|
||||||
|
|
||||||
if (!signerProviders.length) {
|
if (!signerProviders.length) {
|
||||||
return <NoOracleProof type={type} />;
|
return <NoOracleProof type={type} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{signerProviders.map((provider) => (
|
{signerProviders.map((provider) => {
|
||||||
<OracleProfile
|
// Making the assumption here that if the provider name is the same,
|
||||||
key={dataSourceSpecId}
|
// that it is the same provider that the parent market used.
|
||||||
provider={provider}
|
const parentProvider = parentSignerProviders.find(
|
||||||
dataSourceSpecId={dataSourceSpecId}
|
(p) => p.name === provider.name
|
||||||
/>
|
);
|
||||||
))}
|
|
||||||
|
const isParentProviderEqual =
|
||||||
|
parentProvider !== undefined && isEqual(provider, parentProvider);
|
||||||
|
|
||||||
|
// We only want to pass the parent data to the child component if the
|
||||||
|
// data differs from the parent market.
|
||||||
|
return isParentProviderEqual ? (
|
||||||
|
<OracleProfile
|
||||||
|
key={dataSourceSpecId}
|
||||||
|
provider={provider}
|
||||||
|
dataSourceSpecId={dataSourceSpecId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<OracleProfile
|
||||||
|
key={dataSourceSpecId}
|
||||||
|
provider={provider}
|
||||||
|
dataSourceSpecId={dataSourceSpecId}
|
||||||
|
parentProvider={parentProvider}
|
||||||
|
parentDataSourceSpecId={parentDataSourceSpecId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -731,13 +1075,18 @@ const NoOracleProof = ({
|
|||||||
const OracleProfile = (props: {
|
const OracleProfile = (props: {
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
dataSourceSpecId: string;
|
dataSourceSpecId: string;
|
||||||
|
parentProvider?: Provider;
|
||||||
|
parentDataSourceSpecId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
// If this is a successor market, the parent market data will only have been passed
|
||||||
|
// in if it differs from the current data.
|
||||||
const [open, onChange] = useState(false);
|
const [open, onChange] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div key={props.provider.name}>
|
<div key={props.provider.name}>
|
||||||
<OracleBasicProfile
|
<OracleBasicProfile
|
||||||
provider={props.provider}
|
provider={props.provider}
|
||||||
onClick={() => onChange(!open)}
|
onClick={() => onChange(!open)}
|
||||||
|
parentProvider={props.parentProvider}
|
||||||
/>
|
/>
|
||||||
<OracleDialog {...props} open={open} onChange={onChange} />
|
<OracleDialog {...props} open={open} onChange={onChange} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Icon,
|
Icon,
|
||||||
Intent,
|
Intent,
|
||||||
|
Lozenge,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
@ -59,10 +60,12 @@ export const OracleBasicProfile = ({
|
|||||||
provider,
|
provider,
|
||||||
onClick,
|
onClick,
|
||||||
markets: oracleMarkets,
|
markets: oracleMarkets,
|
||||||
|
parentProvider,
|
||||||
}: {
|
}: {
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
markets?: OracleMarketSpecFieldsFragment[] | undefined;
|
markets?: OracleMarketSpecFieldsFragment[] | undefined;
|
||||||
onClick?: (value?: boolean) => void;
|
onClick?: (value?: boolean) => void;
|
||||||
|
parentProvider?: Provider;
|
||||||
}) => {
|
}) => {
|
||||||
const { icon, message, intent } = getVerifiedStatusIcon(provider);
|
const { icon, message, intent } = getVerifiedStatusIcon(provider);
|
||||||
|
|
||||||
@ -78,8 +81,14 @@ export const OracleBasicProfile = ({
|
|||||||
icon: getLinkIcon(proof.type),
|
icon: getLinkIcon(proof.type),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// If this is a successor market and there's a different parent provider,
|
||||||
|
// we'll just show that there's been a change, rather than add old data
|
||||||
|
// in alongside the new provider.
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{parentProvider && (
|
||||||
|
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
|
||||||
|
)}
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
{provider.url && (
|
{provider.url && (
|
||||||
<span className="flex align-items-bottom text-md gap-1">
|
<span className="flex align-items-bottom text-md gap-1">
|
||||||
|
@ -11,16 +11,27 @@ export const OracleDialog = ({
|
|||||||
dataSourceSpecId,
|
dataSourceSpecId,
|
||||||
open,
|
open,
|
||||||
onChange,
|
onChange,
|
||||||
|
parentProvider,
|
||||||
}: {
|
}: {
|
||||||
dataSourceSpecId: string;
|
dataSourceSpecId: string;
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onChange?: (isOpen: boolean) => void;
|
onChange?: (isOpen: boolean) => void;
|
||||||
|
parentProvider?: Provider;
|
||||||
}) => {
|
}) => {
|
||||||
|
// If this is a successor market, the parent market data will only have been passed
|
||||||
|
// in if it differs from the current data. We'll pass this on to the title component
|
||||||
|
// to show a change, but the full profile showing changes is unwieldy - it's enough
|
||||||
|
// to know from the title that the oracle has changed.
|
||||||
const oracleMarkets = useOracleMarkets(provider);
|
const oracleMarkets = useOracleMarkets(provider);
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={<OracleProfileTitle provider={provider} />}
|
title={
|
||||||
|
<OracleProfileTitle
|
||||||
|
provider={provider}
|
||||||
|
parentProvider={parentProvider}
|
||||||
|
/>
|
||||||
|
}
|
||||||
aria-labelledby="oracle-proof-dialog"
|
aria-labelledby="oracle-proof-dialog"
|
||||||
open={open}
|
open={open}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Icon,
|
Icon,
|
||||||
Intent,
|
Intent,
|
||||||
|
Lozenge,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
@ -18,15 +19,30 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
|
export const OracleProfileTitle = ({
|
||||||
|
provider,
|
||||||
|
parentProvider,
|
||||||
|
}: {
|
||||||
|
provider: Provider;
|
||||||
|
parentProvider?: Provider;
|
||||||
|
}) => {
|
||||||
|
// If this is a successor market, the parent provider will only have been passed
|
||||||
|
// in if it differs from the current provider. If it is different, we'll just
|
||||||
|
// show the change in name, not icons and proofs.
|
||||||
const { icon, intent } = getVerifiedStatusIcon(provider);
|
const { icon, intent } = getVerifiedStatusIcon(provider);
|
||||||
const verifiedProofs = provider.proofs.filter(
|
const verifiedProofs = provider.proofs.filter(
|
||||||
(proof) => proof.available === true
|
(proof) => proof.available === true
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
|
{parentProvider && (
|
||||||
|
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
|
||||||
|
)}
|
||||||
{provider.url && (
|
{provider.url && (
|
||||||
<span>
|
<span>
|
||||||
|
{parentProvider && parentProvider.name && (
|
||||||
|
<span className="line-through">{parentProvider.name}</span>
|
||||||
|
)}
|
||||||
<span className="pr-1">{provider.name}</span>
|
<span className="pr-1">{provider.name}</span>
|
||||||
<span className="dark:text-vega-light-300 text-vega-dark-300">
|
<span className="dark:text-vega-light-300 text-vega-dark-300">
|
||||||
({verifiedProofs.length})
|
({verifiedProofs.length})
|
||||||
|
@ -22,7 +22,7 @@ export const getIntentBackground = (intent?: Intent) => {
|
|||||||
return {
|
return {
|
||||||
'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
|
'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
|
||||||
'bg-black dark:bg-white': intent === Intent.None,
|
'bg-black dark:bg-white': intent === Intent.None,
|
||||||
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
|
'bg-vega-blue-300 dark:bg-vega-blue-650': intent === Intent.Primary,
|
||||||
'bg-danger': intent === Intent.Danger,
|
'bg-danger': intent === Intent.Danger,
|
||||||
'bg-warning': intent === Intent.Warning,
|
'bg-warning': intent === Intent.Warning,
|
||||||
// contrast issues with light mode
|
// contrast issues with light mode
|
||||||
|
Loading…
Reference in New Issue
Block a user