Compare commits

..
162 changed files with 2230 additions and 3087 deletions
+28
View File
@@ -0,0 +1,28 @@
---
name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'FEATURE EPIC: '
labels: feature-epic
---
## Core Feature
<Name>
## Tasks
- [ ] Define high level requirements
- [ ] Create basic mockups
- [ ] Update "API Requirements" in core spec
- [ ] Update "User-Interface Spec" in relevant front end repo
- [ ] Create detailed user stories using normal template
## High Level Requirements
## Basic Mockups
## Link to API Requirements in Core spec
## Link to User Interface Specs
## Linked User Stories
+1 -1
View File
@@ -1,6 +1,6 @@
# Related issues 🔗
Closes #[Issue number here]
Issue: #[Issue number here]
# Description
@@ -7,7 +7,7 @@ on:
jobs:
after-release:
runs-on: ubuntu-22.04
timeout-minutes: 30
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -30,18 +30,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Wait for publish to complete
uses: lewagon/wait-on-check-action@v1.3.1
with:
ref: ${{ github.event.release.tag_name }}
check-name: '(CD) publish dist / trading'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- name: resolve ipfs hashes for release
run: |
echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}"
echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
echo "Commit: $commit"
until docker pull vegaprotocol/trading:$commit; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
+1 -1
View File
@@ -6,7 +6,7 @@ name: 'Add Issues To Project Board'
types:
- opened
env:
GH_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }}
GH_TOKEN: ${{ secrets.PROJECT_MANAGE_ACTION }}
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
ISSUE_ID: ${{ github.event.issue.node_id }}
USER: ${{ github.actor }}
+26 -121
View File
@@ -81,6 +81,22 @@ jobs:
with:
main-branch-name: develop
# See affected apps
- name: See affected apps
run: |
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
python3 tools/ci/check-affected.py --github-ref="${{ github.ref }}" --branch-slug="$branch_slug" --event-name="${{ github.event_name }}"
- name: Verify script result
run: |
echo "Check outputs from script"
echo "projects: ${{ env.PROJECTS }}"
echo "projects-e2e: ${{ env.PROJECTS_E2E }}"
echo "preview_governance: ${{ env.PREVIEW_GOVERNANCE }}"
echo "preview_trading: ${{ env.PREVIEW_TRADING }}"
echo "preview_explorer: ${{ env.PREVIEW_EXPLORER }}"
echo "preview_tools: ${{ env.PREVIEW_TOOLS }}"
- name: Check formatting
run: yarn nx format:check
@@ -96,126 +112,6 @@ jobs:
- name: Build affected
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
run: |
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
echo ">>>> debug"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo "Affected: ${affected}"
echo "Branch slug: ${branch_slug}"
echo "Current ref: ${{ github.ref }}"
echo ">>>> eof debug"
projects_array=()
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
# parse if affected is any of three main applications, if none - use all of them
if echo "$affected" | grep -q governance; then
echo "Governance is affected"
projects_array+=("governance")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
fi
if echo "$affected" | grep -q trading; then
echo "Trading is affected"
projects_array+=("trading")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
fi
if echo "$affected" | grep -q explorer; then
echo "Explorer is affected"
projects_array+=("explorer")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
if [[ ${#projects_array[@]} -eq 0 ]]; then
projects_array=("governance" "trading" "explorer")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
# applications parsed before this loop are applicable for running e2e-tests
projects_e2e_array=()
for project in "${projects_array[@]}"; do
projects_e2e_array+=("${project}-e2e")
done
# all applications below this loop are not applicable for running e2e-test
# check if pull request event to deploy tools
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects_array+=("multisig-signer")
fi
# those apps deploy only from develop to mainnet
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on s3"
projects_array+=("multisig-signer")
fi
if echo "$affected" | grep -q static; then
echo "static is affected"
echo "Deploying static on s3"
projects_array+=("static")
fi
if echo "$affected" | grep -q ui-toolkit; then
echo "ui-toolkit is affected"
echo "Deploying ui-toolkit on s3"
projects_array+=("ui-toolkit")
fi
fi
# if branch starts with release/ and ends with trading / governance or explorer - overwrite the array of affected projects with fixed single application
if [[ "${{ github.ref }}" == *release* ]]; then
echo ">> This is a relase branch"
case "${{ github.ref }}" in
*trading)
echo ">> Only trading will be deployed"
projects_array=(trading)
projects_e2e_array=(trading)
;;
*governance)
echo ">> Only governance will be deployed"
projects_array=(governance)
projects_e2e_array=(governance)
;;
*explorer)
echo ">> Only explorer will be deployed"
projects_array=(explorer)
projects_e2e_array=(explorer)
;;
*)
echo ">> All apps will be deployed"
;;
esac
fi
echo "Projects: ${projects_array[@]}"
echo "Projects E2E: ${projects_e2e_array[@]}"
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
echo PROJECTS=$projects_json >> $GITHUB_ENV
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
@@ -224,6 +120,15 @@ jobs:
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }}
console-e2e:
needs: lint-test-build
name: '(CI) console python'
uses: ./.github/workflows/console-test-run.yml
secrets: inherit
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
with:
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
cypress:
needs: lint-test-build
name: '(CI) cypress'
@@ -232,7 +137,7 @@ jobs:
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke @regression'
tags: '@smoke'
publish-dist:
needs: lint-test-build
+110 -27
View File
@@ -1,55 +1,138 @@
name: console-test-run
name: (CI) Console tests
on:
workflow_call:
inputs:
github-sha:
required: true
type: string
jobs:
console-test:
timeout-minutes: 5
runs-on: self-hosted-runner
run-tests:
name: run-tests
runs-on: 8-cores
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
ref: ${{ inputs.github-sha }}
#----------------------------------------------
# cache node modules
#----------------------------------------------
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "restore-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
#----------------------------------------------
# setup node
#----------------------------------------------
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
#----------------------------------------------
# install deps if cache missing
#----------------------------------------------
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
#----------------------------------------------
# build trading
#----------------------------------------------
- name: Build affected spec
run: |
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
#----------------------------------------------
# run trading server
#----------------------------------------------
- name: Run trading server
run: |
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
sleep 5
docker ps
#----------------------------------------------
# check if container persists between runs
#----------------------------------------------
- name: Check server
run: |
docker ps
#----------------------------------------------
# check-out tests repo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
repository: vegaprotocol/console-test
path: './console-test'
- name: Set up Python
#----------------------------------------------
# set-up python
#----------------------------------------------
- name: Set up python
id: setup-python
uses: actions/setup-python@v4
with:
python-version: '3.10.11'
#----------------------------------------------
# ----- install & configure poetry -----
#----------------------------------------------
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
virtualenvs-path: console-test/.venv
#----------------------------------------------
# load cached venv if cache exists
#----------------------------------------------
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: console-test/.venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
#----------------------------------------------
# install dependencies if cache does not exist
#----------------------------------------------
- name: Install dependencies
run: |
poetry install --no-root
working-directory: ./console-test
- name: load Binaries
run: |
poetry run python -m vega_sim.tools.load_binaries
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
#----------------------------------------------
# install vega binaries
#----------------------------------------------
- name: Install vega binaries
working-directory: ./console-test
- name: pull console
run: |
poetry run docker pull ghcr.io/vegaprotocol/frontend/trading:${{ inputs.github-sha }}
- name: Update container_name in config.py
run: |
sed -i "s/container_name = \".*\"/container_name = \"vegaprotocol\/frontend\/trading:${{ inputs.github-sha }}\"/g" config.py
run: poetry run python -m vega_sim.tools.load_binaries --force
#----------------------------------------------
# install playwright
#----------------------------------------------
- name: install playwright
run: poetry run playwright install
working-directory: ./console-test
- name: run tests
run: poetry run pytest --numprocesses auto
#----------------------------------------------
# run tests
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -v -s --numprocesses auto --dist loadfile --durations=20
- name: Check files
run: |
ls -al .
ls -al console-test
#----------------------------------------------
# upload traces
#----------------------------------------------
- name: Upload Playwright Trace
uses: actions/upload-artifact@v3
if: always()
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 100
timeout-minutes: 120
steps:
# Checks if skip cache was requested
- name: Set skip-nx-cache flag
+26 -55
View File
@@ -28,7 +28,13 @@ jobs:
echo IS_MAINNET_RELEASE=false >> $GITHUB_ENV
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
echo IS_S3_RELASE=false >> $GITHUB_ENV
echo IS_S3_RELEASE=false >> $GITHUB_ENV
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
- name: Is dev image
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
run: |
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
- name: Is PR
if: ${{ github.event_name == 'pull_request' }}
@@ -36,7 +42,7 @@ jobs:
echo IS_PR=true >> $GITHUB_ENV
- name: Is mainnet release
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
run: |
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
@@ -51,9 +57,9 @@ jobs:
echo IS_IPFS_RELEASE=true >> $GITHUB_ENV
- name: Is S3 Release
if: ${{ env.IS_IPFS_RELASE == 'false' && github.event_name == 'push' }}
if: ${{ env.IS_IPFS_RELEASE == 'false' && github.event_name == 'push' }}
run: |
echo IS_S3_RELASE=true >> $GITHUB_ENV
echo IS_S3_RELEASE=true >> $GITHUB_ENV
- name: Set up QEMU
id: quemu
@@ -75,7 +81,7 @@ jobs:
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -97,52 +103,13 @@ jobs:
- name: Define dist variables
if: ${{ github.event_name == 'push' }}
run: |
envName=''
domain="vega.rocks"
bucketName=''
python3 tools/ci/define-dist-variables.py --github-ref="${{ github.ref }}" --app="${{ matrix.app }}"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading)
envName="$(echo ${{ github.ref }} | sed -e "s|refs/heads/release/||" | cut -d '-' -f 1 )"
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
envName="mainnet"
bucketName="ui.vega.rocks"
fi
elif [[ "${{ github.ref }}" =~ .*mainnet$ ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
domain="vega.xyz"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
elif [[ "${envName}" = "testnet" ]]; then
domain="fairground.wtf"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
fi
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${envName}.${domain}"
fi
echo "bucket name: ${bucketName}"
echo "env name: ${envName}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
echo ENV_NAME=${envName} >> $GITHUB_ENV
- name: Verify script result
if: ${{ github.event_name == 'push' }}
run: |
echo "BUCKET_NAME=${{ env.BUCKET_NAME }}"
echo "ENV_NAME=${{ env.ENV_NAME }}"
- name: Build local dist
run: |
@@ -157,8 +124,12 @@ jobs:
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
elif [ "${{ matrix.app }}" = "static" ]; then
yarn nx build static || (yarn install && yarn nx build static)
else
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
fi
if [[ -z "$DIST_LOCATION" ]]; then
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
@@ -208,7 +179,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -218,7 +189,7 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
@@ -251,7 +222,7 @@ jobs:
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ env.IS_S3_RELASE == 'true' }}
if: ${{ env.IS_S3_RELEASE == 'true' }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -262,11 +233,11 @@ jobs:
SOURCE_DIR: 'dist-result'
- name: Install aws CLI
if: ${{ env.IS_S3_RELASE == 'true' }}
if: ${{ env.IS_S3_RELEASE == 'true' }}
uses: unfor19/install-aws-cli-action@master
- name: Perform cache invalidation
if: ${{ env.IS_S3_RELASE == 'true' }}
if: ${{ env.IS_S3_RELEASE == 'true' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+1
View File
@@ -15,6 +15,7 @@ on:
- types
- utils
- i18n
- wallet
jobs:
publish:
@@ -320,8 +320,8 @@ context(
// 3001-VOTE-076
cy.getByTestId(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.getByTestId(connectToVegaWalletButton).click();
.and('have.text', 'Connect Vega wallet')
.click();
cy.getByTestId('connector-jsonRpc').click();
cy.getByTestId(vegaWalletNameElement).should('be.visible');
cy.getByTestId(connectToVegaWalletButton).should('not.exist');
@@ -161,7 +161,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('menu-drawer').should('be.visible');
});
it('should have link for proposal page', function () {
it.skip('should have link for proposal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
@@ -1,12 +1,13 @@
/// <reference types="cypress" />
import {
navigateTo,
navigation,
turnTelemetryOff,
waitForSpinner,
} from '../../support/common.functions';
import {
createTenDigitUnixTimeStampForSpecifiedDays,
enterRawProposalBody,
enterUniqueFreeFormProposalBody,
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
@@ -46,11 +47,12 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
.and('contain.text', 'USDC (fake)');
});
it('Unable to submit proposal with public key', function () {
it.skip('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
navigateTo(navigation.proposals);
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('50', 'pub key proposal test');
cy.getByTestId('dialog-content')
.first()
.within(() => {
@@ -78,7 +78,7 @@ context(
cy.getByTestId('connector-jsonRpc')
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.getByTestId('connector-rest')
cy.getByTestId('connector-hosted')
.should('be.visible')
.and('have.text', 'Hosted Fairground wallet');
});
@@ -94,7 +94,7 @@ context(
describe('when rest connector form opened', function () {
before('click hosted wallet app button', function () {
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-rest').click();
cy.getByTestId('connector-hosted').click();
});
});
@@ -2,8 +2,13 @@ import { Button } from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
export const ConnectToVega = () => {
const { appDispatch } = useAppState();
const { t } = useTranslation();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
@@ -11,6 +16,10 @@ export const ConnectToVega = () => {
return (
<Button
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
data-testid="connect-to-vega-wallet-btn"
@@ -3,6 +3,11 @@ import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
interface VegaWalletContainerProps {
children: (key: string) => React.ReactElement;
}
@@ -10,6 +15,7 @@ interface VegaWalletContainerProps {
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
@@ -19,6 +25,10 @@ export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
@@ -71,6 +71,7 @@ export const VegaWallet = () => {
const VegaWalletNotConnected = () => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
@@ -78,6 +79,10 @@ const VegaWalletNotConnected = () => {
<>
<Button
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
fill={true}
@@ -28,6 +28,9 @@ export interface AppState {
/** Total number of VEGA Tokens, both vesting and unlocked, associated for staking */
totalAssociated: BigNumber;
/** Whether or not the connect to VEGA wallet overlay is open */
vegaWalletOverlay: boolean;
/** Whether or not the manage VEGA wallet overlay is open */
vegaWalletManageOverlay: boolean;
@@ -49,7 +52,9 @@ export enum AppStateActionType {
SET_TOKEN,
SET_ALLOWANCE,
REFRESH_BALANCES,
SET_VEGA_WALLET_OVERLAY,
SET_VEGA_WALLET_MANAGE_OVERLAY,
SET_DRAWER,
REFRESH_ASSOCIATED_BALANCES,
SET_ASSOCIATION_BREAKDOWN,
SET_TRANSACTION_OVERLAY,
@@ -64,10 +69,18 @@ export type AppStateAction =
totalSupply: BigNumber;
totalAssociated: BigNumber;
}
| {
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_DRAWER;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_TRANSACTION_OVERLAY;
isOpen: boolean;
@@ -14,6 +14,7 @@ const initialAppState: AppState = {
totalAssociated: new BigNumber(0),
decimals: 0,
totalSupply: new BigNumber(0),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
@@ -30,10 +31,23 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
totalAssociated: action.totalAssociated,
};
}
case AppStateActionType.SET_VEGA_WALLET_OVERLAY: {
return {
...state,
vegaWalletOverlay: action.isOpen,
};
}
case AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY: {
return {
...state,
vegaWalletManageOverlay: action.isOpen,
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
vegaWalletOverlay: false,
};
}
case AppStateActionType.SET_TRANSACTION_OVERLAY: {
+1 -1
View File
@@ -7,9 +7,9 @@ import {
const urlParams = new URLSearchParams(window.location.search);
export const injected = new InjectedConnector();
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const Connectors = {
@@ -10,7 +10,10 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import {
AppStateActionType,
useAppState,
} from '../../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../../lib/bignumber';
import { DATE_FORMAT_LONG } from '../../../../lib/date-formats';
import { VoteState } from './use-user-vote';
@@ -70,6 +73,7 @@ export const VoteButtons = ({
dialog: Dialog,
}: VoteButtonsProps) => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { pubKey } = useVegaWallet();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
@@ -94,6 +98,10 @@ export const VoteButtons = ({
<div data-testid="connect-wallet">
<ButtonLink
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
@@ -134,6 +142,7 @@ export const VoteButtons = ({
minVoterBalance,
spamProtectionMinTokens,
t,
appDispatch,
openVegaWalletDialog,
]);
@@ -14,6 +14,7 @@ const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
@@ -2,9 +2,14 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { SubHeading } from '../../components/heading';
export const ConnectToSeeRewards = () => {
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
@@ -21,6 +26,10 @@ export const ConnectToSeeRewards = () => {
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
@@ -8,9 +8,10 @@ import { TxState } from '../../../hooks/transaction-reducer';
import { useTransaction } from '../../../hooks/use-transaction';
import { BigNumber } from '../../../lib/bignumber';
import { AssociateInfo } from './associate-info';
import { removeDecimal, toBigNum } from '@vegaprotocol/utils';
import { toBigNum } from '@vegaprotocol/utils';
import type { EthereumConfig } from '@vegaprotocol/web3';
import { useBalances } from '../../../lib/balances/balances-store';
import { MaxUint256 } from '@ethersproject/constants';
export const WalletAssociate = ({
perform,
@@ -42,7 +43,7 @@ export const WalletAssociate = ({
} = useTransaction(() => {
return token.approve(
ethereumConfig.staking_bridge_contract.address,
removeDecimal('1000000', decimals).toString()
MaxUint256.toString()
);
});
+12 -9
View File
@@ -2,6 +2,7 @@ import { removeDecimal } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
import {
OrderStatusMapping,
OrderTimeInForceMapping,
OrderTypeMapping,
Side,
} from '@vegaprotocol/types';
@@ -16,6 +17,7 @@ const orderStatus = 'status';
const orderRemaining = 'remaining';
const orderPrice = 'price';
const orderTimeInForce = 'timeInForce';
const orderCreatedAt = 'createdAt';
const orderUpdatedAt = 'updatedAt';
const assetSelectField = 'select[name="asset"]';
const amountField = 'input[name="amount"]';
@@ -120,7 +122,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.get('main[data-testid="/portfolio"]').should('exist');
cy.getByTestId(collateralTab).click();
cy.getByTestId('open-transfer').click();
cy.getByTestId('open-transfer-dialog').click();
cy.getByTestId('transfer-form').should('be.visible');
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
cy.get('select option')
@@ -187,21 +189,19 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
// 0006-NETW-010
const market = this.market;
cy.visit(`/#/markets/${market.id}`);
cy.getByTestId('node-health-trigger').realHover();
cy.getByTestId('node-health')
.children()
.first()
.should('contain.text', 'Operational')
.next()
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname)
.next()
.then(($el) => {
const blockHeight = parseInt($el.text());
// block height will increase over the course of the test run so best
// we can do here is check that its showing something sensible
expect(blockHeight).to.be.greaterThan(0);
});
cy.getByTestId('node-health')
.children()
.eq(1)
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname);
});
it('can place and receive an order', function () {
@@ -260,7 +260,10 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
OrderStatusMapping.STATUS_ACTIVE
);
cy.get(`[col-id='${orderRemaining}']`).should('contain.text', '0.00');
cy.get(`[col-id='${orderRemaining}']`).should(
'contain.text',
`0.00/${order.size}`
);
cy.get(`[col-id='${orderPrice}']`).then(($price) => {
expect(parseFloat($price.text())).to.equal(parseFloat(order.price));
@@ -268,10 +271,10 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.get(`[col-id='${orderTimeInForce}']`).should(
'contain.text',
'GTC'
OrderTimeInForceMapping[order.timeInForce]
);
checkIfDataAndTimeOfCreationAndUpdateIsEqual(orderUpdatedAt);
checkIfDataAndTimeOfCreationAndUpdateIsEqual(orderCreatedAt);
});
});
});
@@ -20,8 +20,8 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.get('main[data-testid="/portfolio"]').should('exist');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.wait('@Assets');
connectEthereumWallet('MetaMask');
}
before(() => {
@@ -102,6 +102,8 @@ describe('deposit actions', { tags: '@smoke' }, () => {
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/markets/market-1');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
});
it('Deposit to trade is visble', () => {
@@ -1,6 +1,5 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const nodeHealthTrigger = 'node-health-trigger';
describe('home', { tags: '@regression' }, () => {
before(() => {
@@ -9,23 +8,22 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
describe('node health', () => {
describe('footer', () => {
it('shows current block height', () => {
// 0006-NETW-004
// 0006-NETW-008
// 0006-NETW-009
cy.getByTestId(nodeHealthTrigger).realHover();
cy.getByTestId(nodeHealth)
.children()
.first()
.should('contain.text', 'Operational', {
timeout: 10000,
})
.next()
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname)
.next()
.should('contain.text', '100'); // all mocked queries have x-block-height header set to 100
cy.getByTestId(nodeHealth)
.children()
.eq(1)
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname);
});
it('shows node switcher details', () => {
@@ -34,7 +32,7 @@ describe('home', { tags: '@regression' }, () => {
// 0006-NETW-014
// 0006-NETW-015
// 0006-NETW-016
cy.getByTestId(nodeHealthTrigger).click();
cy.getByTestId(nodeHealth).click();
cy.getByTestId(dialogContent).should('contain.text', 'Connected node');
cy.getByTestId(dialogContent).should(
'contain.text',
@@ -58,7 +56,7 @@ describe('home', { tags: '@regression' }, () => {
// 0006-NETW-018
// 0006-NETW-019
// 0006-NETW-020
cy.getByTestId(nodeHealthTrigger).click();
cy.getByTestId(nodeHealth).click();
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('node-url-custom').click();
cy.getByTestId('connect').should('be.disabled');
@@ -68,15 +68,14 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)');
validateMarketDataRow(1, 'Market ID', 'market-0');
validateMarketDataRow(2, 'Parent Market ID', 'market-1');
validateMarketDataRow(
3,
2,
'Trading Mode',
MarketTradingModeMapping.TRADING_MODE_CONTINUOUS
);
validateMarketDataRow(4, 'Market Decimal Places', '5');
validateMarketDataRow(5, 'Position Decimal Places', '0');
validateMarketDataRow(6, 'Settlement Asset Decimal Places', '5');
validateMarketDataRow(3, 'Market Decimal Places', '5');
validateMarketDataRow(4, 'Position Decimal Places', '0');
validateMarketDataRow(5, 'Settlement Asset Decimal Places', '5');
});
it('instrument displayed', () => {
@@ -132,10 +132,9 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
it('can see header title', () => {
// 5002-LIQP-004
// 5002-LIQP-005
cy.getByTestId('header-title').should(
'contain.text',
'BTCUSD.MF21 liquidity provision'
);
cy.getByTestId('header-title')
.should('contain.text', 'BTCUSD.MF21 liquidity provision')
.and('contain.text', 'Go to trading');
});
it('can see target stake', () => {
@@ -172,7 +171,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId('indicator').should('be.visible');
cy.getByTestId(itemValue).should('have.text', ' 0.10%').realHover();
cy.getByTestId(itemValue).should('have.text', '0.10%').realHover();
});
});
});
@@ -22,12 +22,15 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
});
// 6001-MARK-066
it('can open popover to view markets', () => {
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('be.visible');
});
@@ -37,30 +40,29 @@ describe('markets selector', { tags: '@smoke' }, () => {
const data = [
{
code: 'SOLUSD',
markPrice: 'XYZalpha84.41',
markPrice: '84.41XYZalpha',
change: '',
vol: '24h vol0.00',
vol: '0.0024h vol',
},
{
code: 'ETHBTC.QM21',
markPrice: 'tBTC46,126.90058',
markPrice: '46,126.90058tBTC',
change: '',
vol: '24h vol0.00',
vol: '0.0024h vol',
},
{
code: 'BTCUSD.MF21',
markPrice: 'tDAI46,126.90058',
markPrice: '46,126.90058tDAI',
change: '',
vol: '24h vol0.00',
vol: '0.0024h vol',
},
{
code: 'AAPL.MF21',
markPrice: 'tUSDC46,126.90058',
markPrice: '46,126.90058tUSDC',
change: '',
vol: '24h vol0.00',
vol: '0.0024h vol',
},
];
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId(list)
.find('a')
.each((item, i) => {
@@ -84,9 +86,18 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
});
it('can use the filter options', () => {
cy.getByTestId('header-title').should('be.visible').click();
it('can see all markets link', () => {
// 6001-MARK-026
cy.getByTestId('market-selector').within(() => {
cy.getByTestId('all-markets-link')
.should('be.visible')
.and('have.text', 'All markets')
.and('have.attr', 'href')
.and('contain', '#/markets/all');
});
});
it('can use the filter options', () => {
// 6001-MARK-027
// product type
cy.getByTestId('product-Spot').click();
@@ -107,8 +118,6 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
it('can sort by by top gaining and top losing market', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-030
// 6001-MARK-031
// 6001-MARK-032
@@ -126,8 +135,6 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
it('can filter by settlement asset', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-028
cy.getByTestId('asset-trigger').click();
cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
@@ -0,0 +1,71 @@
describe('market bottom panel', { tags: '@smoke' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
});
it('on xxl screen should be splitted out into two tables', () => {
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.viewport(1801, 1000);
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('Fills').click();
cy.getByTestId('Collateral').click();
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-accounts').should('have.attr', 'data-state', 'active');
});
});
@@ -99,6 +99,9 @@ describe('Navbar', { tags: '@smoke' }, () => {
cy.getByTestId('menu-drawer').should('not.be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer')
.find('[data-testid="Settings"]')
.should('be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('not.be.visible');
});
@@ -1,10 +1,10 @@
const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894185';
const askPrice = 'price-9894585';
const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894185';
const askVolume = 'ask-vol-9894585';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185';
const askCumulative = 'cumulative-vol-9894585';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution';
@@ -33,7 +33,7 @@ describe('order book', { tags: '@smoke' }, () => {
it('show orders prices', () => {
// 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94185');
cy.getByTestId(askPrice).should('have.text', '98.94585');
cy.getByTestId(bidPrice).should('have.text', '98.89001');
});
@@ -45,7 +45,7 @@ describe('order book', { tags: '@smoke' }, () => {
it('show prices cumulative volumes', () => {
// 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '38');
cy.getByTestId(askCumulative).should('have.text', '39');
cy.getByTestId(bidCumulative).should('have.text', '7');
});
@@ -71,7 +71,7 @@ describe('order book', { tags: '@smoke' }, () => {
it('copy price to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94185');
cy.getByTestId(dealTicketPrice).should('have.value', '98.94585');
});
it('change price resolution', () => {
+33 -20
View File
@@ -1,29 +1,42 @@
describe('Settings page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Settings') {
cy.get('[data-testid="sidebar"] [data-testid="Settings"]').click();
}
cy.clearLocalStorage().then(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
cy.get('[aria-label="cog icon"]').click();
});
});
it('telemetry checkbox should work well', () => {
const telemetrySwitch = '#switch-settings-telemetry-switch';
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.location('hash').should('equal', '#/settings');
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'unchecked'
);
cy.get('[for="telemetry-approval"]').click();
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'checked'
);
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'checked'
);
cy.get('[for="telemetry-approval"]').click();
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'unchecked'
);
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'unchecked'
);
});
});
@@ -12,7 +12,7 @@ describe(
'account validation',
{ tags: '@regression', testIsolation: true },
() => {
describe.skip('zero balance error', () => {
describe('zero balance error', () => {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
@@ -74,8 +74,8 @@ describe(
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('sidebar-content')
.find('h2')
cy.getByTestId('dialog-content')
.find('h1')
.eq(0)
.should('have.text', 'Deposit');
});
@@ -431,8 +431,6 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
});
const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => {
// 7003-MORD-007
// 7003-MORD-012
@@ -10,7 +10,6 @@ describe('trades', { tags: '@smoke' }, () => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
@@ -28,50 +27,37 @@ describe('trades', { tags: '@smoke' }, () => {
it('show trades prices', () => {
// 6005-THIS-003
cy.getByTestId(tradesTable)
.get(`${colIdPrice} ${colHeader}`)
.first()
.should('have.text', 'Price');
cy.getByTestId(tradesTable)
.get(colIdPrice)
.each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.getByTestId(tradesTable)
.get(`${colIdSize} ${colHeader}`)
.first()
.should('have.text', 'Size');
cy.getByTestId(tradesTable)
.get(colIdSize)
.each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
// This won't pass in CI, but does locally
it.skip('show trades date and time', () => {
it('show trades date and time', () => {
// 6005-THIS-005
cy.getByTestId(tradesTable) // order table shares identical col id
.find(`${colIdCreatedAt} ${colHeader}`)
.should('have.text', 'Created at');
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.getByTestId(tradesTable)
.get(`.ag-center-cols-container ${colIdCreatedAt}`)
.each(($tradeDateTime) => {
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
});
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.getByTestId(tradesTable)
.find(colIdCreatedAt)
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
@@ -85,15 +71,9 @@ describe('trades', { tags: '@smoke' }, () => {
});
});
// this passes locally but doesn't in CI
it.skip('copy price to deal ticket form', () => {
cy.getByTestId('order-type-TYPE_LIMIT').click(); // make sure on limit
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.getByTestId(tradesTable)
.find(colIdPrice)
.last()
.should('be.visible')
.click();
cy.get(colIdPrice).last().should('be.visible').click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
@@ -63,7 +63,7 @@ describe(
cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.find('[data-testid="connector-hosted"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
@@ -89,7 +89,7 @@ describe(
);
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.find('[data-testid="connector-hosted"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password');
@@ -100,7 +100,7 @@ describe(
it('doesnt connect with empty fields', () => {
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.find('[data-testid="connector-hosted"]')
.click();
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
@@ -5,16 +5,20 @@ const amountShortName = 'input[name="amount"] + div + span.text-xs';
const assetSelection = 'select-asset';
const assetBalance = 'asset-balance';
const assetOption = 'rich-select-option';
const transferText = 'transfer-intro-text';
const closeDialog = 'dialog-close';
const dialogTitle = 'dialog-title';
const dialogTransferText = 'dialog-transfer-text';
const dropdownMenu = 'dropdown-menu';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const keyID = '[data-testid="dialog-transfer-text"] > .rounded-md';
const manageVegaWallet = 'manage-vega-wallet';
const openTransferButton = 'open-transfer';
const openTransferDialog = 'open-transfer-dialog';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transfer = 'transfer';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
@@ -35,18 +39,14 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.visit('/#/portfolio');
cy.getByTestId('Trading').first().click();
cy.getByTestId(collateralTab).click();
cy.getByTestId(dropdownMenu).first().click();
cy.getByTestId(transfer).click();
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Transfer') {
cy.get('[data-testid="sidebar"] [data-testid="Transfer"]').click();
}
});
cy.wait('@Assets');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
@@ -72,18 +72,21 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
cy.getByTestId(dialogTitle).click();
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
cy.getByTestId(dialogTitle).click();
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
cy.getByTestId(dialogTitle).click();
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
@@ -131,7 +134,6 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
@@ -152,7 +154,7 @@ describe(
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
cy.getByTestId(dialogTransferText)
.should('exist')
.get(keyID)
.invoke('text')
@@ -202,6 +204,7 @@ describe(
'contain.text',
'You cannot transfer more than your available collateral'
);
cy.getByTestId(closeDialog).click();
});
}
);
@@ -214,7 +217,7 @@ describe('withdraw actions', { tags: '@smoke', testIsolation: true }, () => {
cy.visit('/#/portfolio');
cy.getByTestId(collateralTab).click();
cy.getByTestId(openTransferButton).click();
cy.getByTestId(openTransferDialog).click();
cy.wait('@Accounts');
cy.wait('@Assets');
@@ -21,7 +21,7 @@ describe('withdraw form validation', { tags: '@smoke' }, () => {
cy.visit('/#/portfolio');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click(); // sidebar item
cy.getByTestId('withdraw-dialog-button').click();
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
@@ -87,17 +87,15 @@ describe(
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click();
cy.getByTestId('withdraw-dialog-button').click();
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
+1 -1
View File
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.21-core-0.71.6
NX_APP_VERSION=v0.20.23-core-0.71.6
@@ -14,13 +14,20 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { Tab, Tabs, Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
Tab,
Tabs,
Link as UiToolkitLink,
Indicator,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { memo, useEffect, useState } from 'react';
import { Header, HeaderStat, HeaderTitle } from '../../components/header';
import { useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/markets';
import { DocsLinks } from '@vegaprotocol/environment';
@@ -64,15 +71,19 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
return (
<Header
title={
market?.tradableInstrument.instrument.name &&
market?.tradableInstrument.instrument.code &&
marketId && (
<HeaderTitle>
{market.tradableInstrument.instrument.code &&
t(
'%s liquidity provision',
market.tradableInstrument.instrument.code
)}
</HeaderTitle>
<HeaderTitle
primaryContent={`${market.tradableInstrument.instrument.code} ${t(
'liquidity provision'
)}`}
secondaryContent={
<Link to={Links[Routes.MARKET](marketId)}>
<UiToolkitLink>{t('Go to trading')}</UiToolkitLink>
</Link>
}
/>
)
}
>
@@ -105,7 +116,9 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
</div>
</HeaderStat>
<HeaderStat heading={t('Liquidity supplied')} testId="liquidity-supplied">
<Indicator variant={status} /> {formatNumberPercentage(percentage, 2)}
<Indicator variant={status} />
{formatNumberPercentage(percentage, 2)}
</HeaderStat>
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
<div className="break-word">{marketId}</div>
@@ -156,34 +169,24 @@ export const LiquidityViewContainer = ({
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<LiquidityViewHeader marketId={marketId} />
<div className="p-1">
<div className="h-full border border-default">
<Tabs value={tab || LiquidityTabs.Active} onValueChange={setTab}>
<Tab
id={LiquidityTabs.MyLiquidityProvision}
name={t('My liquidity provision')}
hidden={!pubKey}
>
<LiquidityContainer
marketId={marketId}
filter={{ partyId: pubKey || undefined }}
/>
</Tab>
<Tab id={LiquidityTabs.Active} name={t('Active')}>
<LiquidityContainer
marketId={marketId}
filter={{ active: true }}
/>
</Tab>
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
<LiquidityContainer
marketId={marketId}
filter={{ active: false }}
/>
</Tab>
</Tabs>
</div>
</div>
<Tabs value={tab || LiquidityTabs.Active} onValueChange={setTab}>
<Tab
id={LiquidityTabs.MyLiquidityProvision}
name={t('My liquidity provision')}
hidden={!pubKey}
>
<LiquidityContainer
marketId={marketId}
filter={{ partyId: pubKey || undefined }}
/>
</Tab>
<Tab id={LiquidityTabs.Active} name={t('Active')}>
<LiquidityContainer marketId={marketId} filter={{ active: true }} />
</Tab>
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
<LiquidityContainer marketId={marketId} filter={{ active: false }} />
</Tab>
</Tabs>
</div>
);
};
@@ -9,15 +9,13 @@ import {
DropdownMenuSeparator,
} from '@vegaprotocol/ui-toolkit';
type Assets = Array<{ id: string; symbol: string }>;
export const AssetDropdown = ({
assets,
checkedAssets,
onSelect,
onReset,
}: {
assets: Assets | undefined;
assets: Array<{ id: string; symbol: string }> | undefined;
checkedAssets: string[];
onSelect: (id: string, checked: boolean) => void;
onReset: () => void;
@@ -30,7 +28,7 @@ export const AssetDropdown = ({
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="asset-trigger">
<TriggerText assets={assets} checkedAssets={checkedAssets} />
<span className="px-1">$</span>
</DropdownMenuTrigger>
}
>
@@ -58,23 +56,3 @@ export const AssetDropdown = ({
</DropdownMenu>
);
};
const TriggerText = ({
assets,
checkedAssets,
}: {
assets: Assets;
checkedAssets: string[];
}) => {
let text = t('Asset');
if (checkedAssets.length === 1) {
const assetId = checkedAssets[0];
const asset = assets.find((a) => a.id === assetId);
text = asset ? asset.symbol : t('Asset (1)');
} else if (checkedAssets.length > 1) {
text = t(`Asset (${checkedAssets.length})`);
}
return <span className="px-1">{text}</span>;
};
@@ -5,85 +5,92 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/markets';
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header';
import { MarketMarkPrice } from '../../components/market-mark-price';
import { HeaderStatMarketTradingMode } from '../../components/market-trading-mode';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/markets';
import { MarketState } from '../../components/market-state';
import { HeaderStatMarketTradingMode } from '../../components/market-trading-mode';
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
interface MarketHeaderStatsProps {
interface HeaderStatsProps {
market: Market | null;
}
export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
export const HeaderStats = ({ market }: HeaderStatsProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
return (
<>
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
<div className="flex flex-col justify-end lg:pt-4">
<div className="xl:flex xl:gap-4 items-end">
<div
data-testid="header-summary"
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</div>
</div>
</div>
);
};
@@ -1,4 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketSelectorItem } from './market-selector-item';
import { MemoryRouter } from 'react-router-dom';
@@ -90,6 +91,8 @@ describe('MarketSelectorItem', () => {
},
];
const mockOnSelect = jest.fn();
const renderJsx = (mocks: MockedResponse[]) => {
return render(
<MemoryRouter>
@@ -98,6 +101,7 @@ describe('MarketSelectorItem', () => {
market={market}
currentMarketId={market.id}
style={{}}
onSelect={mockOnSelect}
/>
</MockedProvider>
</MemoryRouter>
@@ -169,7 +173,7 @@ describe('MarketSelectorItem', () => {
// link renders and is styled
expect(link).toHaveAttribute('href', '/markets/' + market.id);
expect(link.parentNode).toHaveClass('bg-vega-light-100');
expect(link).toHaveClass('ring-1');
expect(screen.getByTitle('24h vol')).toHaveTextContent('0.00');
expect(screen.getByTitle(symbol)).toHaveTextContent('-');
@@ -179,6 +183,13 @@ describe('MarketSelectorItem', () => {
expect(screen.getByTitle(symbol)).toHaveTextContent(
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
});
await userEvent.click(link);
expect(mockOnSelect).toHaveBeenCalledWith(market.id);
});
});
@@ -1,7 +1,11 @@
import type { CSSProperties } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
formatNumber,
priceChangePercentage,
} from '@vegaprotocol/utils';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { calcCandleVolume } from '@vegaprotocol/markets';
import { useCandles } from '@vegaprotocol/markets';
@@ -17,22 +21,29 @@ export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
// 'py-1 px-2',
const wrapperClasses = classNames(
'block bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4',
'min-h-[120px]',
{
'ring-1 ring-vega-light-300 dark:ring-vega-dark-300':
currentMarketId === market.id,
}
);
return (
<div style={style} role="row">
<div style={style} className="my-0.5 px-4">
<Link
to={`/markets/${market.id}`}
className={classNames('h-full flex items-center gap-2 px-4', {
'hover:bg-vega-clight-700 dark:hover:bg-vega-cdark-700':
market.id !== currentMarketId,
'bg-vega-clight-600 dark:bg-vega-cdark-600':
market.id === currentMarketId,
})}
className={wrapperClasses}
onClick={() => {
onSelect && onSelect(market.id);
}}
>
<MarketData market={market} />
</Link>
@@ -80,41 +91,87 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
return (
<>
<div className="w-1/4" role="gridcell">
<h3 className="text-ellipsis whitespace-nowrap overflow-hidden">
<div className="flex items-end gap-1 mb-1">
<h3
className={classNames(
'overflow-hidden text-ellipsis whitespace-nowrap',
{
'w-1/2': mode, // make space for showing the trading mode
}
)}
>
{market.tradableInstrument.instrument.code}
</h3>
{mode && (
<p className="text-xs text-vega-orange-500 dark:text-vega-orange-550 whitespace-nowrap">
<p className="w-1/2 text-xs text-right text-vega-orange-500 dark:text-vega-orange-550">
{mode}
</p>
)}
</div>
<div
className="w-1/4 text-sm"
title={instrument.product.settlementAsset.symbol}
data-testid="market-selector-data-row"
role="gridcell"
>
{price} {instrument.product.settlementAsset.symbol}
</div>
<div
className="w-1/4 text-sm text-right"
title={t('24h vol')}
data-testid="market-selector-data-row"
role="gridcell"
>
{volume}
</div>
<div className="w-1/4" role="gridcell">
<DataRow value={volume} label={t('24h vol')} />
<DataRow
value={price}
label={instrument.product.settlementAsset.symbol}
/>
<div className="relative text-xs p-1">
{oneDayCandles && (
<Sparkline
width={70}
height={15}
data={oneDayCandles.map((c) => Number(c.close))}
/>
<PriceChange candles={oneDayCandles.map((c) => c.close)} />
)}
<div
// absolute so height is not larger than price change value
className="absolute right-0 bottom-0 w-[120px]"
>
{oneDayCandles && (
<Sparkline
width={120}
height={20}
data={oneDayCandles.map((c) => Number(c.close))}
/>
)}
</div>
</div>
</>
);
};
const DataRow = ({
value,
label,
}: {
value: string | ReactNode;
label: string;
}) => {
return (
<div
className="text-ellipsis whitespace-nowrap overflow-hidden leading-tight"
data-testid="market-selector-data-row"
>
<span title={label} className="text-sm mr-1">
{value}
</span>
<span className="text-xs text-vega-light-300 dark:text-vega-light-300">
{label}
</span>
</div>
);
};
const PriceChange = ({ candles }: { candles: string[] }) => {
const priceChange = candles ? priceChangePercentage(candles) : undefined;
const priceChangeClasses = classNames('text-xs', {
'text-market-red': priceChange && priceChange < 0,
'text-market-green-600 dark:text-market-green':
priceChange && priceChange > 0,
});
let prefix = '';
if (priceChange && priceChange > 0) {
prefix = '+';
}
const formattedChange = formatNumber(Number(priceChange), 2);
return (
<div className={priceChangeClasses} data-testid="market-item-change">
{priceChange ? `${prefix}${formattedChange}%` : '-'}
</div>
);
};
@@ -143,6 +143,7 @@ describe('MarketSelector', () => {
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.getByRole('link')).toHaveTextContent('All markets');
});
it('filters by product type', async () => {
@@ -9,7 +9,9 @@ import {
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useMarketSelectorList } from './use-market-selector-list';
import type { ProductType } from './product-selector';
import { Product, ProductSelector } from './product-selector';
@@ -17,7 +19,6 @@ import { AssetDropdown } from './asset-dropdown';
import type { SortType } from './sort-dropdown';
import { Sort, SortDropdown } from './sort-dropdown';
import { MarketSelectorItem } from './market-selector-item';
import classNames from 'classnames';
export type Filter = {
searchTerm: string;
@@ -47,15 +48,18 @@ export const MarketSelector = ({
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
<div data-testid="market-selector">
<div className="pt-2 px-2 mb-2 w-[320px] lg:w-[584px]">
<div
className="grid grid-rows-[min-content_1fr_min-content] h-full"
data-testid="market-selector"
>
<div className="px-4 pt-2 pb-4">
<ProductSelector
product={filter.product}
onSelect={(product) => {
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="text-sm flex gap-1 items-stretch">
<div className="flex-1">
<Input
onChange={(e) =>
@@ -72,7 +76,7 @@ export const MarketSelector = ({
onClick={() =>
setFilter((curr) => ({ ...curr, searchTerm: '' }))
}
className="text-secondary"
className="text-vega-light-200 dark:text-vega-dark-200"
>
<VegaIcon name={VegaIconNames.CROSS} />
</button>
@@ -145,6 +149,18 @@ export const MarketSelector = ({
}
/>
</div>
<div className="px-4 py-2">
<span className="inline-block border-b border-black dark:border-white">
<Link
to={'/markets/all'}
data-testid="all-markets-link"
className="flex items-center gap-x-2"
>
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
</span>
</div>
</div>
);
};
@@ -169,34 +185,21 @@ const MarketList = ({
return <div>{error.message}</div>;
}
return (
<TinyScroll>
<div
className={classNames(
'flex gap-2',
'bg-vega-clight-700 dark:bg-vega-cdark-700',
'p-2 mx-2 border-b border-default text-xs text-secondary'
)}
>
<div className="w-1/4" role="columnheader">
{t('Name')}
</div>
<div className="w-1/4" role="columnheader">
{t('Price')}
</div>
<div className="w-1/4 text-right" role="columnheader">
{t('24h volume')}
</div>
<div className="w-1/4" role="columnheader" />
</div>
<List
data={data}
loading={loading}
height={400}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
/>
</TinyScroll>
<AutoSizer>
{({ width, height }) => (
<TinyScroll>
<List
data={data}
loading={loading}
width={width}
height={height}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
/>
</TinyScroll>
)}
</AutoSizer>
);
};
@@ -219,18 +222,21 @@ const ListItem = ({
market={data.data[index]}
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
/>
);
const List = ({
data,
loading,
width,
height,
onSelect,
noItems,
currentMarketId,
}: ListItemData & {
loading: boolean;
width: number;
height: number;
noItems: string;
}) => {
@@ -244,7 +250,7 @@ const List = ({
);
if (!data || loading) {
return (
<div style={{ height }}>
<div style={{ width, height }}>
<Skeleton />
<Skeleton />
</div>
@@ -253,20 +259,24 @@ const List = ({
if (!data.length) {
return (
<div style={{ height }} data-testid="no-items">
<div className="mx-4 my-2 text-sm">{noItems}</div>
<div style={{ width, height }} data-testid="no-items">
<div className="mb-2 px-4">
<div className="text-sm bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg px-4 py-2">
{noItems}
</div>
</div>
</div>
);
}
return (
<FixedSizeList
className="vega-scrollbar"
className="virtualized-list"
itemCount={data.length}
itemData={itemData}
itemSize={45}
itemSize={130}
itemKey={itemKey}
width="100%"
width={width}
height={height}
>
{ListItem}
+1 -9
View File
@@ -14,7 +14,6 @@ import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { ViewType, useSidebar } from '../../components/sidebar';
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
return markPrice && decimalPlaces
@@ -62,7 +61,7 @@ const TitleUpdater = ({
export const MarketPage = () => {
const { marketId } = useParams();
const navigate = useNavigate();
const { init, view, setView } = useSidebar();
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const update = useGlobalStore((store) => store.update);
@@ -82,13 +81,6 @@ export const MarketPage = () => {
}
}, [update, lastMarketId, data?.id]);
// Make sidebar open on deal ticket by default
useEffect(() => {
if (init && view === null) {
setView({ type: ViewType.Order });
}
}, [init, view, setView]);
const tradeView = useMemo(() => {
if (largeScreen) {
return (
@@ -1,8 +1,4 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Routes } from '../../pages/client-router';
import { t } from '@vegaprotocol/i18n';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
// Make sure these match the available __typename properties on product
export const Product = {
@@ -29,12 +25,12 @@ export const ProductSelector = ({
onSelect: (product: ProductType) => void;
}) => {
return (
<div className="flex mb-2">
<div className="flex gap-3 mb-3">
{Object.keys(Product).map((t) => {
const classes = classNames('px-3 py-1.5 rounded', {
'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
t === product,
'text-secondary': t !== product,
const classes = classNames('py-1 border-b-2', {
'border-vega-yellow text-black dark:text-white': t === product,
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
t !== product,
});
return (
<button
@@ -49,10 +45,6 @@ export const ProductSelector = ({
</button>
);
})}
<Link to={Routes.MARKETS} className="flex items-center gap-2 ml-auto">
<span className="underline underline-offset-4">{t('All markets')}</span>
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
</div>
);
};
@@ -8,6 +8,8 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
export const Sort = {
@@ -41,9 +43,7 @@ export const SortDropdown = ({
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="sort-trigger">
{currentSort === SortTypeMapping.None
? t('Sort')
: SortTypeMapping[currentSort]}
<VegaIcon name={VegaIconNames.TREND_UP} />
</DropdownMenuTrigger>
}
>
+221 -90
View File
@@ -1,5 +1,6 @@
import { memo } from 'react';
import { memo, useState } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -8,8 +9,8 @@ import { t } from '@vegaprotocol/i18n';
import { OracleBanner } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { Filter } from '@vegaprotocol/orders';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import {
Popover,
Tab,
LocalStoragePersistTabs as Tabs,
VegaIcon,
@@ -17,22 +18,168 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { Header, HeaderTitle } from '../../components/header';
import { HeaderTitle } from '../../components/header';
import {
ResizableGrid,
ResizableGridPanel,
usePaneLayout,
} from '../../components/resizable-grid';
import { TradingViews } from './trade-views';
import { MarketSuccessorBanner } from '../../components/market-banner';
import { MarketSelector } from './market-selector';
import { MarketHeaderStats } from './market-header-stats';
import { HeaderStats } from './header-stats';
interface TradeGridProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
pinnedAsset?: PinnedAsset;
}
interface BottomPanelProps {
marketId: string;
pinnedAsset?: PinnedAsset;
}
const MarketBottomPanel = memo(
({ marketId, pinnedAsset }: BottomPanelProps) => {
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'bottom' });
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
return 'xxxl' === screenSize ? (
<ResizableGrid
proportionalLayout
minSize={200}
onChange={handleOnLayoutChange}
>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={sizes[0] || '50%'}
minSize={50}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom-left">
<Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
/>
</VegaWalletContainer>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
/>
</VegaWalletContainer>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
/>
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.fills.component onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={sizes[1] || '50%'}
minSize={50}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom-right">
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.positions.component
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
) : (
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.positions.component onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
/>
</VegaWalletContainer>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
/>
</VegaWalletContainer>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
/>
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.fills.component onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
);
}
);
MarketBottomPanel.displayName = 'MarketBottomPanel';
const MainGrid = memo(
({
marketId,
@@ -41,6 +188,7 @@ const MainGrid = memo(
marketId: string;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
id: 'middle-1',
@@ -55,6 +203,25 @@ const MainGrid = memo(
minSize={200}
onChange={handleOnMiddleLayoutChange}
>
<ResizableGridPanel
preferredSize={sizesMiddle[0] || 330}
minSize={300}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-center">
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.ticket.component
marketId={marketId}
onMarketClick={onMarketClick}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab>
<Tab id="info" name={t('Info')}>
<TradingViews.info.component marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
@@ -96,63 +263,7 @@ const MainGrid = memo(
preferredSize={sizes[1] || '25%'}
minSize={50}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.positions.component
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
/>
</VegaWalletContainer>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
/>
</VegaWalletContainer>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
/>
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.fills.component
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
<MarketBottomPanel marketId={marketId} pinnedAsset={pinnedAsset} />
</ResizableGridPanel>
</ResizableGrid>
);
@@ -161,34 +272,61 @@ const MainGrid = memo(
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_min-content_1fr]'
'grid-rows-[min-content_min-content_1fr]',
'grid-cols-[320px_1fr]'
);
const paneWrapperClasses = classNames('min-h-0', {
'col-span-2 col-start-1': !sidebarOpen,
});
return (
<div className={wrapperClasses}>
<Header
title={
<Popover
trigger={
<HeaderTitle>
{market?.tradableInstrument.instrument.code}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
</HeaderTitle>
}
<div className="border-b border-r border-default">
<div className="h-full flex gap-2 justify-between items-end px-4 pt-1 pb-3">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button
onClick={() => setSidebarOpen((x) => !x)}
className="flex flex-col items-center text-xs w-12"
data-testid="sidebar-toggle"
>
<MarketSelector currentMarketId={market?.id} />
</Popover>
}
>
<MarketHeaderStats market={market} />
</Header>
<div className="bg-vega-green">
<MarketSuccessorBanner market={market} />
{sidebarOpen ? (
<>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
<span className="text-vega-light-300 dark:text-vega-dark-300">
{t('Close')}
</span>
</>
) : (
<>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
<span className="text-vega-light-300 dark:text-vega-dark-300">
{t('Markets')}
</span>
</>
)}
</button>
</div>
</div>
<div className="border-b border-default min-w-0">
<HeaderStats market={market} />
</div>
<div className="col-span-2">
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="min-h-0 py-0.5">
{sidebarOpen && (
<div className="border-r border-default min-h-0">
<div className="h-full pb-8">
<MarketSelector currentMarketId={market?.id} />
</div>
</div>
)}
<div className={paneWrapperClasses}>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
@@ -201,16 +339,9 @@ interface TradeGridChildProps {
const TradeGridChild = ({ children }: TradeGridChildProps) => {
return (
<section className="h-full p-1">
<section className="h-full">
<AutoSizer>
{({ width, height }) => (
<div
style={{ width, height }}
className="border border-default rounded-sm"
>
{children}
</div>
)}
{({ width, height }) => <div style={{ width, height }}>{children}</div>}
</AutoSizer>
</section>
);
@@ -9,7 +9,7 @@ import type { TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { memo, useState } from 'react';
import {
Popover,
Icon,
Splash,
VegaIcon,
VegaIconNames,
@@ -17,10 +17,10 @@ import {
import { NO_MARKET } from './constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { Header, HeaderTitle } from '../../components/header';
import { HeaderStats } from './header-stats';
import * as DialogPrimitives from '@radix-ui/react-dialog';
import { HeaderTitle } from '../../components/header';
import { MarketSelector } from './market-selector';
import { MarketSuccessorBanner } from '../../components/market-banner';
import { MarketHeaderStats } from './market-header-stats';
interface TradePanelsProps {
market: Market | null;
@@ -37,6 +37,7 @@ export const TradePanels = ({
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
@@ -71,24 +72,26 @@ export const TradePanels = ({
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<Header
title={
<Popover
trigger={
<HeaderTitle>
{market?.tradableInstrument.instrument.code}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</HeaderTitle>
}
>
<MarketSelector currentMarketId={market?.id} />
</Popover>
}
>
<MarketHeaderStats market={market} />
</Header>
<div className="border-b border-default min-w-0">
<div className="flex gap-4 items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button onClick={() => setDrawerOpen((x) => !x)} className="p-2">
<span
className={classNames('block', {
'rotate-90 translate-x-1': !drawerOpen,
'-rotate-90 -translate-x-1': drawerOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
<HeaderStats market={market} />
</div>
<div>
<MarketSuccessorBanner market={market} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
@@ -104,7 +107,8 @@ export const TradePanels = ({
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
@@ -118,6 +122,25 @@ export const TradePanels = ({
);
})}
</div>
<DialogPrimitives.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
<DialogPrimitives.Portal>
<DialogPrimitives.Overlay />
<DialogPrimitives.Content
className={classNames(
'fixed h-full max-w-[500px] w-[90vw] z-10 top-0 left-0 transition-transform',
'bg-white dark:bg-black',
'border-r border-default'
)}
>
<DialogPrimitives.Close className="absolute top-0 right-0 p-2">
<Icon name="cross" />
</DialogPrimitives.Close>
{drawerOpen && (
<MarketSelector onSelect={() => setDrawerOpen(false)} />
)}
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>
</div>
);
};
@@ -1,5 +1,7 @@
import type { ComponentProps } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/markets';
import { TradesContainer } from '@vegaprotocol/trades';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
@@ -16,6 +18,8 @@ import { OrdersContainer } from '../../components/orders-container';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
@@ -43,6 +47,14 @@ export const TradingViews = {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
@@ -15,20 +15,16 @@ export const MarketsPage = () => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<div className="h-full pt-0.5 pb-3 px-1">
<div className="h-full my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}>
<Markets />
</Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
<Closed />
</Tab>
</Tabs>
</div>
</div>
<Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}>
<Markets />
</Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
<Closed />
</Tab>
</Tabs>
);
};
@@ -1,12 +1,11 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { DepositsTable } from '@vegaprotocol/deposits';
import { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useSidebar, ViewType } from '../../components/sidebar';
export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
@@ -16,7 +15,7 @@ export const DepositsContainer = () => {
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const setView = useSidebar((store) => store.setView);
const openDepositDialog = useDepositDialog((state) => state.open);
return (
<div className="h-full">
<DepositsTable
@@ -25,11 +24,11 @@ export const DepositsContainer = () => {
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
/>
{!isReadOnly && (
<div className="h-auto flex justify-end p-2 bottom-0 right-0 absolute dark:bg-black/75 bg-white/75 rounded">
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
onClick={() => setView({ type: ViewType.Deposit })}
onClick={() => openDepositDialog()}
data-testid="deposit-button"
>
{t('Deposit')}
@@ -21,7 +21,6 @@ import {
ResizableGridPanel,
usePaneLayout,
} from '../../components/resizable-grid';
import { ViewType, useSidebar } from '../../components/sidebar';
const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals();
@@ -29,14 +28,13 @@ const WithdrawalsIndicator = () => {
return null;
}
return (
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
<span className="bg-vega-blue-450 text-white text-[10px] rounded p-[3px] pb-[2px] leading-none">
{ready.length}
</span>
);
};
export const Portfolio = () => {
const { init, view, setView } = useSidebar();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
@@ -45,16 +43,9 @@ export const Portfolio = () => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle]);
// Make transfer sidebar open by default
useEffect(() => {
if (init && view === null) {
setView({ type: ViewType.Transfer });
}
}, [init, view, setView]);
const onMarketClick = useMarketClickHandler(true);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'py-0.5 h-full max-h-full flex flex-col';
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
<div className={wrapperClasses}>
<ResizableGrid vertical onChange={handleOnLayoutChange}>
@@ -127,8 +118,8 @@ interface PortfolioGridChildProps {
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
return (
<section className="h-full p-1">
<div className="border border-default h-full rounded-sm">{children}</div>
<section className="bg-white dark:bg-black w-full h-full">
{children}
</section>
);
};
@@ -1,6 +1,7 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import {
withdrawalProvider,
useWithdrawalDialog,
WithdrawalsTable,
useIncompleteWithdrawals,
} from '@vegaprotocol/withdraws';
@@ -8,7 +9,6 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { ViewType, useSidebar } from '../../components/sidebar';
export const WithdrawalsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet();
@@ -17,7 +17,7 @@ export const WithdrawalsContainer = () => {
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const setView = useSidebar((store) => store.setView);
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
const { ready, delayed } = useIncompleteWithdrawals();
return (
@@ -32,11 +32,11 @@ export const WithdrawalsContainer = () => {
/>
</div>
{!isReadOnly && (
<div className="h-auto flex justify-end p-2 bottom-0 right-0 absolute dark:bg-black/75 bg-white/75 rounded">
<div className="h-auto flex justify-end px-[11px] py-2 bottom-0 right-3 absolute dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
onClick={() => setView({ type: ViewType.Withdraw })}
onClick={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
@@ -0,0 +1,2 @@
export { Settings as default } from './settings';
export { SettingsButton } from './settings-button';
@@ -0,0 +1,16 @@
import { Icon, NavigationLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Links, Routes } from '../../pages/client-router';
import { IconNames } from '@blueprintjs/icons';
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
return (
<NavigationLink data-testid="Settings" to={Links[Routes.SETTINGS]()}>
{withMobile ? (
t('Settings')
) : (
<Icon name={IconNames.COG} className="!align-middle" />
)}
</NavigationLink>
);
};
@@ -0,0 +1,52 @@
import { t } from '@vegaprotocol/i18n';
import { TelemetryApproval } from '../../components/welcome-dialog/telemetry-approval';
import {
Divider,
RoundedWrapper,
Switch,
ThemeSwitcher,
ToastPositionSetter,
} from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
export const Settings = () => {
const { theme, setTheme } = useThemeSwitcher();
const text = t(theme === 'dark' ? 'Light mode' : 'Dark mode');
return (
<div className="py-16 px-8 flex w-full justify-center">
<div className="lg:min-w-[700px] min-w-[300px]">
<h1 className="text-4xl xl:text-5xl uppercase font-alpha calt">
{t('Settings')}
</h1>
<div className="mt-8 text-base text-neutral-500 dark:text-neutral-400">
{t('Changes are applied automatically.')}
</div>
<div className="mt-10 w-full">
<RoundedWrapper paddingBottom>
<div className="flex justify-between py-3">
<div className="flex shrink">
<ThemeSwitcher />
<label htmlFor="theme-switcher" className="self-center text-lg">
{text}
</label>
</div>
<Switch
name="settings-theme-switch"
onCheckedChange={() => setTheme()}
checked={theme === 'dark'}
/>
</div>
<Divider />
<TelemetryApproval
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data.'
)}
/>
<Divider />
<ToastPositionSetter />
</RoundedWrapper>
</div>
</div>
</div>
);
};
@@ -1,17 +1,18 @@
import { useCallback } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useWithdrawalDialog } from '@vegaprotocol/withdraws';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager } from '@vegaprotocol/accounts';
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
import { useDepositDialog } from '@vegaprotocol/deposits';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { ViewType, useSidebar } from '../sidebar';
export const AccountsContainer = ({
pinnedAsset,
@@ -24,7 +25,9 @@ export const AccountsContainer = ({
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const setView = useSidebar((store) => store.setView);
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
const openDepositDialog = useDepositDialog((store) => store.open);
const openTransferDialog = useTransferDialog((store) => store.open);
const gridStore = useAccountStore((store) => store.gridStore);
const updateGridStore = useAccountStore((store) => store.updateGridStore);
@@ -52,34 +55,27 @@ export const AccountsContainer = ({
<AccountManager
partyId={pubKey}
onClickAsset={onClickAsset}
onClickWithdraw={(assetId) => {
setView({ type: ViewType.Withdraw, assetId });
}}
onClickDeposit={(assetId) => {
setView({ type: ViewType.Deposit, assetId });
}}
onClickTransfer={(assetId) => {
setView({ type: ViewType.Transfer, assetId });
}}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
gridProps={gridStoreCallbacks}
/>
{!isReadOnly && !hideButtons && (
<div className="flex gap-2 justify-end p-2 absolute bottom-0 right-0 dark:bg-black/75 bg-white/75 rounded">
<div className="flex gap-2 justify-end p-2 px-[11px] absolute lg:fixed bottom-0 right-3 dark:bg-black/75 bg-white/75 rounded">
<Button
variant="primary"
size="sm"
data-testid="open-transfer"
onClick={() => setView({ type: ViewType.Transfer })}
data-testid="open-transfer-dialog"
onClick={() => openTransferDialog()}
>
{t('Transfer')}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => setView({ type: ViewType.Deposit })}
onClick={() => openDepositDialog()}
>
{t('Deposit')}
</Button>
@@ -6,7 +6,7 @@ export const AnnouncementBanner = () => {
// Return an empty div so that the grid layout in _app.page.ts
// renders correctly
if (!ANNOUNCEMENTS_CONFIG_URL) {
return null;
return <div />;
}
return <Banner app="console" configUrl={ANNOUNCEMENTS_CONFIG_URL} />;
@@ -0,0 +1,70 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
import { MockedProvider } from '@apollo/client/testing';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetNodeSwitcher = jest.fn();
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn().mockImplementation(() => ({
VEGA_URL: 'https://vega-url.wtf',
VEGA_INCIDENT_URL: 'https://blog.vega.community',
})),
useNodeSwitcherStore: jest.fn(() => mockSetNodeSwitcher),
}));
describe('NodeHealth', () => {
it('controls the node switcher dialog', async () => {
render(<NodeHealth />, { wrapper: MockedProvider });
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
expect(mockSetNodeSwitcher).toHaveBeenCalled();
});
it('External link to blog should be present', () => {
render(<NodeHealth />, { wrapper: MockedProvider });
expect(
screen.getByRole('link', { name: /^Mainnet status & incidents/ })
).toBeInTheDocument();
});
});
describe('NodeUrl', () => {
it('renders correct part of node url', () => {
const node = 'https://api.n99.somenetwork.vega.xyz';
render(<NodeUrl url={node} />);
expect(
screen.getByText('api.n99.somenetwork.vega.xyz')
).toBeInTheDocument();
});
});
describe('HealthIndicator', () => {
const cases = [
{
intent: Intent.Success,
text: 'Operational',
classname: 'bg-vega-green-550',
},
{
intent: Intent.Warning,
text: '5 Blocks behind',
classname: 'bg-warning',
},
{ intent: Intent.Danger, text: 'Non operational', classname: 'bg-danger' },
];
it.each(cases)(
'renders correct text and indicator color for $diff block difference',
(elem) => {
render(<HealthIndicator text={elem.text} intent={elem.intent} />);
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
expect(screen.getByText(elem.text)).toBeInTheDocument();
}
);
});
+119
View File
@@ -0,0 +1,119 @@
import { useCallback } from 'react';
import {
useEnvironment,
useNodeHealth,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
export const Footer = () => {
return (
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
{/* Pull left to align with top nav, due to button padding */}
<div className="-ml-2">
<NodeHealth />
</div>
</footer>
);
};
export const NodeHealth = () => {
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const { datanodeBlockHeight, text, intent } = useNodeHealth();
const onClick = useCallback(() => {
setNodeSwitcher(true);
}, [setNodeSwitcher]);
const incidentsLink = VEGA_INCIDENT_URL && (
<ExternalLink className="ml-1" href={VEGA_INCIDENT_URL}>
{t('Mainnet status & incidents')}
</ExternalLink>
);
return (
<>
{VEGA_URL && (
<FooterButton onClick={onClick} data-testid="node-health">
<FooterButtonPart>
<HealthIndicator text={text} intent={intent} />
</FooterButtonPart>
<FooterButtonPart>
<NodeUrl url={VEGA_URL} />
</FooterButtonPart>
{/* create a monospace effect - avoiding jumps of width */}
<FooterButtonPart
width={`${
datanodeBlockHeight
? String(datanodeBlockHeight).length + 'ch'
: 'auto'
}`}
>
<span title={t('Block height')}>{datanodeBlockHeight}</span>
</FooterButtonPart>
</FooterButton>
)}
{incidentsLink}
</>
);
};
interface NodeUrlProps {
url: string;
}
export const NodeUrl = ({ url }: NodeUrlProps) => {
const urlObj = new URL(url);
const nodeUrl = urlObj.hostname;
return <span title={t('Connected node')}>{nodeUrl}</span>;
};
interface HealthIndicatorProps {
text: string;
intent: Intent;
}
export const HealthIndicator = ({ text, intent }: HealthIndicatorProps) => {
return (
<span title={t('Node health')}>
<Indicator variant={intent} />
{text}
</span>
);
};
type FooterButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
const FooterButton = (props: FooterButtonProps) => {
const buttonClasses = classNames(
'px-2 py-0.5 rounded-md',
'enabled:hover:bg-vega-light-150',
'dark:enabled:hover:bg-vega-dark-150'
);
return <button {...props} className={buttonClasses} />;
};
const FooterButtonPart = ({
width = 'auto',
children,
}: {
children: ReactNode;
width?: string;
}) => {
return (
<span
style={{ width }}
className={classNames(
'relative inline-block mr-2 last:mr-0 pr-2 last:pr-0',
'last:after:hidden',
'after:content after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2',
'after:h-3 after:w-1 after:border-r',
'after:border-vega-light-300 dark:after:border-vega-dark-300'
)}
>
{children}
</span>
);
};
+1
View File
@@ -0,0 +1 @@
export * from './footer';
+36 -32
View File
@@ -1,30 +1,28 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import type { ReactElement, ReactNode } from 'react';
import { Children } from 'react';
import { cloneElement } from 'react';
interface TradeMarketHeaderProps {
title: ReactNode;
children: ReactNode;
children: Array<ReactElement | null>;
}
export const Header = ({ title, children }: TradeMarketHeaderProps) => {
const headerClasses = classNames(
'grid',
'grid-rows-[min-content_min-content]',
'xl:grid-cols-[min-content_1fr]',
'lg:border-x border-b border-default',
'bg-vega-clight-800 dark:bg-vega-cdark-800'
);
return (
<header className="lg:px-1">
<div className={headerClasses}>
<div className="flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
{title}
</div>
<div data-testid="header-summary" className="min-w-0">
<div className="px-3 lg:px-4 py-2 flex flex-nowrap gap-4 items-center text-xs overflow-x-auto">
{children}
</div>
<header className="w-screen xl:px-4 pt-2 border-b border-default">
<div className="xl:flex xl:gap-4 items-end">
<div className="px-4 xl:px-0 pb-2 xl:pb-3">{title}</div>
<div
data-testid="header-summary"
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
>
{Children.map(children, (child, index) => {
if (!child) return null;
return cloneElement(child, {
id: `header-stat-${index}`,
});
})}
</div>
</div>
</header>
@@ -44,11 +42,9 @@ export const HeaderStat = ({
description?: string | ReactNode;
testId?: string;
}) => {
const itemClass = classNames(
'text-muted',
'min-w-min last:pr-0 whitespace-nowrap'
);
const itemValueClasses = 'text-default';
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default first:border-none text-neutral-500 dark:text-neutral-400';
const itemHeading = 'text-black dark:text-white';
return (
<div data-testid={testId} className={itemClass}>
@@ -59,7 +55,7 @@ export const HeaderStat = ({
<div
data-testid="item-value"
aria-labelledby={id}
className={itemValueClasses}
className={itemHeading}
>
{children}
</div>
@@ -68,13 +64,21 @@ export const HeaderStat = ({
);
};
export const HeaderTitle = ({ children }: { children: ReactNode }) => {
export const HeaderTitle = ({
primaryContent,
secondaryContent,
}: {
primaryContent: ReactNode;
secondaryContent: ReactNode;
}) => {
return (
<h1
data-testid="header-title"
className="flex gap-4 items-center text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default"
>
{children}
</h1>
<div className="text-left" data-testid="header-title">
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap !leading-[1]">
{primaryContent}
</div>
<div className="text-xs whitespace-nowrap text-vega-light-300 dark:text-vega-dark-300">
{secondaryContent}
</div>
</div>
);
};
-1
View File
@@ -1 +0,0 @@
export * from './layout-with-sidebar';
@@ -1,38 +0,0 @@
import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames';
export const LayoutWithSidebar = () => {
const sidebarView = useSidebar((store) => store.view);
const sidebarOpen = sidebarView !== null;
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-cols-[1fr_45px]',
'lg:grid-cols-[1fr_350px_45px]'
);
return (
<div className={gridClasses}>
<section
className={classNames('col-start-1 col-end-1', {
'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
})}
>
<Outlet />
</section>
<div
// min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller
className={classNames('col-start-1 lg:col-start-2 min-h-0', {
hidden: !sidebarOpen,
})}
>
<SidebarContent />
</div>
<div className="col-start-2 lg:col-start-3 bg-vega-clight-800 dark:bg-vega-cdark-800 border-l border-default">
<Sidebar />
</div>
</div>
);
};
@@ -156,7 +156,8 @@ export const MarketLiquiditySupplied = ({
description={description}
testId="liquidity-supplied"
>
<Indicator variant={status} /> {supplied} (
<Indicator variant={status} />
{supplied} (
{percentage.gt(100) ? '>100%' : formatNumberPercentage(percentage, 2)})
</HeaderStat>
) : (
@@ -1 +0,0 @@
export * from './market-successor-banner';
@@ -1,188 +0,0 @@
import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/react-testing';
import * as dataProviders from '@vegaprotocol/data-provider';
import { MarketSuccessorBanner } from './market-successor-banner';
import * as Types from '@vegaprotocol/types';
import * as allUtils from '@vegaprotocol/utils';
import type { Market } from '@vegaprotocol/markets';
import type { PartialDeep } from 'type-fest';
const market = {
id: 'marketId',
tradableInstrument: {
instrument: {
metadata: {
tags: [],
},
},
},
marketTimestamps: {
close: null,
},
successorMarketID: 'successorMarketID',
} as unknown as Market;
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn().mockImplementation((args) => {
if (args.skip) {
return {
data: null,
error: null,
};
}
return {
data: mockDataSuccessorMarket,
error: null,
};
}),
}));
jest.mock('@vegaprotocol/utils', () => ({
...jest.requireActual('@vegaprotocol/utils'),
getMarketExpiryDate: jest.fn(),
}));
let mockCandles = {};
jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'),
useCandles: () => mockCandles,
}));
describe('MarketSuccessorBanner', () => {
beforeEach(() => {
jest.clearAllMocks();
mockDataSuccessorMarket = {
id: 'successorMarketID',
state: Types.MarketState.STATE_ACTIVE,
tradingMode: Types.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
instrument: {
name: 'Successor Market Name',
},
},
};
});
describe('should be hidden', () => {
it('when no market', () => {
const { container } = render(<MarketSuccessorBanner market={null} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
});
it('when no successorMarketID', () => {
const amendedMarket = {
...market,
successorMarketID: null,
};
const { container } = render(
<MarketSuccessorBanner market={amendedMarket} />,
{
wrapper: MockedProvider,
}
);
expect(container).toBeEmptyDOMElement();
expect(dataProviders.useDataProvider).lastCalledWith(
expect.objectContaining({ skip: true })
);
});
it('no successor market data', () => {
mockDataSuccessorMarket = null;
const { container } = render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
expect(dataProviders.useDataProvider).lastCalledWith(
expect.objectContaining({
variables: { marketId: 'successorMarketID' },
skip: false,
})
);
});
it('successor market not in continuous mode', () => {
mockDataSuccessorMarket = {
...mockDataSuccessorMarket,
tradingMode: Types.MarketTradingMode.TRADING_MODE_NO_TRADING,
};
const { container } = render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
expect(dataProviders.useDataProvider).lastCalledWith(
expect.objectContaining({
variables: { marketId: 'successorMarketID' },
skip: false,
})
);
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
});
it('successor market is not active', () => {
mockDataSuccessorMarket = {
...mockDataSuccessorMarket,
state: Types.MarketState.STATE_PENDING,
};
const { container } = render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
expect(dataProviders.useDataProvider).lastCalledWith(
expect.objectContaining({
variables: { marketId: 'successorMarketID' },
skip: false,
})
);
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
});
});
describe('should be displayed', () => {
it('should be rendered', () => {
render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(
screen.getByText('This market has been succeeded')
).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'Successor Market Name' })
).toHaveAttribute('href', '/#/markets/successorMarketID');
});
it('should display optionally successor volume', () => {
mockDataSuccessorMarket = {
...mockDataSuccessorMarket,
positionDecimalPlaces: 3,
};
mockCandles = {
oneDayCandles: [
{ volume: 123 },
{ volume: 456 },
{ volume: 789 },
{ volume: 99999 },
],
};
render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument();
});
it('should display optionally duration', () => {
jest
.spyOn(allUtils, 'getMarketExpiryDate')
.mockReturnValue(
new Date(Date.now() + 24 * 60 * 60 * 1000 + 60 * 1000)
);
render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(
screen.getByText(/^This market expires in 1 day/)
).toBeInTheDocument();
});
});
});
@@ -1,115 +0,0 @@
import { useState } from 'react';
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import {
calcCandleVolume,
marketProvider,
useCandles,
} from '@vegaprotocol/markets';
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
getMarketExpiryDate,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
const getExpiryDate = (tags: string[], close?: string): Date | null => {
const expiryDate = getMarketExpiryDate(tags);
return expiryDate || (close && new Date(close)) || null;
};
export const MarketSuccessorBanner = ({
market,
}: {
market: Market | null;
}) => {
const { data: successorData } = useDataProvider({
dataProvider: marketProvider,
variables: {
marketId: market?.successorMarketID || '',
},
skip: !market?.successorMarketID,
});
const [visible, setVisible] = useState(true);
const expiry = market
? getExpiryDate(
market.tradableInstrument.instrument.metadata.tags || [],
market.marketTimestamps.close
)
: null;
const duration =
expiry && isBefore(new Date(), expiry)
? intervalToDuration({ start: new Date(), end: expiry })
: null;
const isInContinuesMode =
successorData?.state === Types.MarketState.STATE_ACTIVE &&
successorData?.tradingMode ===
Types.MarketTradingMode.TRADING_MODE_CONTINUOUS;
const { oneDayCandles } = useCandles({
marketId: successorData?.id,
});
const candleVolume = oneDayCandles?.length
? calcCandleVolume(oneDayCandles)
: null;
const successorVolume =
candleVolume && isNumeric(successorData?.positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
successorData?.positionDecimalPlaces as number
)
: null;
if (isInContinuesMode && visible) {
return (
<NotificationBanner
intent={Intent.Primary}
onClose={() => {
setVisible(false);
}}
>
<div className="uppercase mb-1">
{t('This market has been succeeded')}
</div>
<div>
{duration && (
<span>
{t('This market expires in %s.', [
formatDuration(duration, {
format: [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
],
}),
])}
</span>
)}{' '}
{t('The successor market')}{' '}
<ExternalLink href={`/#/markets/${successorData?.id}`}>
{successorData?.tradableInstrument.instrument.name}
</ExternalLink>
{successorVolume && (
<span> {t('has %s 24h vol.', [successorVolume])}</span>
)}
</div>
</NotificationBanner>
);
}
return null;
};
+15 -1
View File
@@ -24,6 +24,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { SettingsButton } from '../../client-pages/settings';
import {
ProtocolUpgradeCountdown,
ProtocolUpgradeCountdownMode,
@@ -44,13 +45,14 @@ export const Navbar = ({
return (
<Navigation
appName="console"
appName="Console"
theme={theme}
actions={
<>
<ProtocolUpgradeCountdown
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<SettingsButton />
<VegaWalletConnectButton />
</>
}
@@ -118,6 +120,18 @@ export const Navbar = ({
</NavigationItem>
)}
</NavigationList>
<NavigationList
className="[.drawer-content_&]:border-t [.drawer-content_&]:border-t-vega-light-200 dark:[.drawer-content_&]:border-t-vega-dark-200 [.drawer-content_&]:pt-4 [.drawer-content_&]:mt-4"
hide={[
NavigationBreakpoint.Small,
NavigationBreakpoint.Narrow,
NavigationBreakpoint.Full,
]}
>
<NavigationItem className="[.drawer-content_&]:w-full">
<SettingsButton withMobile />
</NavigationItem>
</NavigationList>
</Navigation>
);
};
@@ -1 +0,0 @@
export * from './node-health';
@@ -1,62 +0,0 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NodeHealthContainer, NodeUrl } from './node-health';
import { MockedProvider } from '@apollo/client/testing';
const mockSetNodeSwitcher = jest.fn();
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn().mockImplementation(() => ({
VEGA_URL: 'https://vega-url.wtf',
VEGA_INCIDENT_URL: 'https://blog.vega.community',
})),
useNodeSwitcherStore: jest.fn(() => mockSetNodeSwitcher),
}));
describe('NodeHealthContainer', () => {
it('controls the node switcher dialog', async () => {
render(<NodeHealthContainer />, { wrapper: MockedProvider });
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
expect(mockSetNodeSwitcher).toHaveBeenCalled();
});
it('Shows node health data on hover', async () => {
render(<NodeHealthContainer />, { wrapper: MockedProvider });
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
await userEvent.hover(screen.getByRole('button'));
await waitFor(() => {
const portal = within(
document.querySelector(
'[data-radix-popper-content-wrapper]'
) as HTMLElement
);
// two tooltips get rendered, I believe for animation purposes
const tooltip = within(portal.getAllByTestId('tooltip-content')[0]);
expect(
tooltip.getByRole('link', { name: /^Mainnet status & incidents/ })
).toBeInTheDocument();
expect(tooltip.getByText('Non operational')).toBeInTheDocument();
expect(tooltip.getByTitle('Connected node')).toHaveTextContent(
'vega-url.wtf'
);
});
});
});
describe('NodeUrl', () => {
it('renders correct part of node url', () => {
const node = 'https://api.n99.somenetwork.vega.xyz';
render(<NodeUrl url={node} />);
expect(
screen.getByText('api.n99.somenetwork.vega.xyz')
).toBeInTheDocument();
});
});
@@ -1,62 +0,0 @@
import {
useEnvironment,
useNodeHealth,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { Tooltip } from '../../components/tooltip';
export const NodeHealthContainer = () => {
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const { text, intent, datanodeBlockHeight } = useNodeHealth();
return (
<Tooltip
description={
<div
className="flex flex-col gap-2 p-4 text-sm"
data-testid="node-health"
>
<div className="flex items-center gap-2">
<Indicator variant={intent} />
<p>{text}</p>
<p>{datanodeBlockHeight}</p>
</div>
{VEGA_URL && (
<p>
<NodeUrl url={VEGA_URL} />
</p>
)}
{VEGA_INCIDENT_URL && (
<ExternalLink href={VEGA_INCIDENT_URL}>
{t('Mainnet status & incidents')}
</ExternalLink>
)}
</div>
}
align="end"
side="right"
sideOffset={10}
>
<button
className="flex justify-center items-center p-2 rounded hover:bg-vega-light-200 hover:dark:bg-vega-dark-200"
onClick={() => setNodeSwitcher(true)}
data-testid="node-health-trigger"
>
<Indicator variant={intent} size="lg" />
</button>
</Tooltip>
);
};
interface NodeUrlProps {
url: string;
}
export const NodeUrl = ({ url }: NodeUrlProps) => {
const urlObj = new URL(url);
const nodeUrl = urlObj.hostname;
return <span title={t('Connected node')}>{nodeUrl}</span>;
};
@@ -1 +0,0 @@
export * from './settings';
@@ -1,56 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import type { ReactNode } from 'react';
export const Settings = () => {
const { theme, setTheme } = useThemeSwitcher();
const [isApproved, setIsApproved] = useTelemetryApproval();
return (
<div>
<SettingsGroup label={t('Dark mode')}>
<Switch
name="settings-theme-switch"
onCheckedChange={() => setTheme()}
checked={theme === 'dark'}
/>
</SettingsGroup>
<SettingsGroup
label={t('Share usage data')}
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data.'
)}
>
<Switch
name="settings-telemetry-switch"
onCheckedChange={(isOn) => setIsApproved(isOn)}
checked={isApproved}
/>
</SettingsGroup>
<SettingsGroup label={t('Toast location')}>
<ToastPositionSetter />
</SettingsGroup>
</div>
);
};
const SettingsGroup = ({
label,
helpText,
children,
}: {
label: string;
helpText?: string;
children: ReactNode;
}) => {
return (
<div className="flex justify-between items-start mb-4">
<div className="w-3/4">
<label className="text-sm">{label}</label>
{helpText && <p className="text-muted text-xs">{helpText}</p>}
</div>
{children}
</div>
);
};
-1
View File
@@ -1 +0,0 @@
export * from './sidebar';
@@ -1,139 +0,0 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Sidebar, SidebarContent, ViewType, useSidebar } from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />,
}));
jest.mock('@vegaprotocol/accounts', () => ({
TransferContainer: () => <div data-testid="transfer" />,
}));
jest.mock('@vegaprotocol/deposits', () => ({
DepositContainer: () => <div data-testid="deposit" />,
}));
jest.mock('../settings', () => ({
Settings: () => <div data-testid="settings" />,
}));
describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])(
'does not render ticket and info',
(path) => {
render(
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
);
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument();
// no order or info sidebars
expect(screen.queryByTestId(ViewType.Order)).not.toBeInTheDocument();
expect(screen.queryByTestId(ViewType.Info)).not.toBeInTheDocument();
}
);
it('renders ticket and info on market pages', () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
);
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument();
// order and info sidebars are shown
expect(screen.getByTestId(ViewType.Order)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Info)).toBeInTheDocument();
});
it('renders selected state', async () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
);
const settingsButton = screen.getByTestId(ViewType.Settings);
const orderButton = screen.getByTestId(ViewType.Order);
// select settings first
await userEvent.click(settingsButton);
expect(settingsButton).toHaveClass('bg-vega-yellow text-black');
// switch to order
await userEvent.click(orderButton);
expect(settingsButton).not.toHaveClass('bg-vega-yellow text-black');
expect(orderButton).toHaveClass('bg-vega-yellow text-black');
// close order
await userEvent.click(orderButton);
expect(orderButton).not.toHaveClass('bg-vega-yellow text-black');
});
});
describe('SidebarContent', () => {
it('renders the correct content', () => {
const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
);
expect(container).toBeEmptyDOMElement();
act(() => {
useSidebar.setState({ view: { type: ViewType.Transfer } });
});
expect(screen.getByTestId('transfer')).toBeInTheDocument();
act(() => {
useSidebar.setState({ view: { type: ViewType.Deposit } });
});
expect(screen.getByTestId('deposit')).toBeInTheDocument();
});
it('closes sidebar if market id is required but not present', () => {
const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
);
act(() => {
useSidebar.setState({ view: { type: ViewType.Order } });
});
expect(container).toBeEmptyDOMElement();
act(() => {
useSidebar.setState({ view: { type: ViewType.Settings } });
});
expect(screen.getByTestId('settings')).toBeInTheDocument();
act(() => {
useSidebar.setState({ view: { type: ViewType.Info } });
});
expect(container).toBeEmptyDOMElement();
});
});
-290
View File
@@ -1,290 +0,0 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { Route, Routes, useParams } from 'react-router-dom';
import { create } from 'zustand';
import { TransferContainer } from '@vegaprotocol/accounts';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { DepositContainer } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { MarketInfoAccordionContainer } from '@vegaprotocol/markets';
import { TinyScroll, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { NodeHealthContainer } from '../node-health';
import { Settings } from '../settings';
import { Tooltip } from '../../components/tooltip';
import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware';
const STORAGE_KEY = 'vega_sidebar_store';
export enum ViewType {
Order = 'Order',
Info = 'Info',
Deposit = 'Deposit',
Withdraw = 'Withdraw',
Transfer = 'Transfer',
Settings = 'Settings',
}
type SidebarView =
| {
type: ViewType.Deposit;
assetId?: string;
}
| {
type: ViewType.Withdraw;
assetId?: string;
}
| {
type: ViewType.Transfer;
assetId?: string;
}
| {
type: ViewType.Order;
}
| {
type: ViewType.Info;
}
| {
type: ViewType.Settings;
};
export const Sidebar = () => {
return (
<div className="flex flex-col gap-2 h-full py-1" data-testid="sidebar">
<nav className="flex flex-col items-center gap-4 p-1">
{/* sidebar options that always show */}
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
/>
{/* buttons for specific routes */}
<Routes>
<Route
path={AppRoutes.MARKETS}
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
element={null}
/>
<Route
// render nothing for portfolio
path={AppRoutes.PORTFOLIO}
element={null}
/>
<Route
path={AppRoutes.MARKET}
element={
<>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
/>
</>
}
/>
</Routes>
</nav>
<nav className="mt-auto flex flex-col items-center gap-4 p-1">
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
/>
<NodeHealthContainer />
</nav>
</div>
);
};
const SidebarButton = ({
view,
icon,
tooltip,
}: {
view: ViewType;
icon: VegaIconNames;
tooltip: string;
}) => {
const { currView, setView } = useSidebar((store) => ({
currView: store.view,
setView: store.setView,
}));
const buttonClasses = classNames('flex items-center p-1 rounded', {
'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500':
view !== currView?.type,
'bg-vega-yellow hover:bg-vega-yellow-550 text-black':
view === currView?.type,
});
return (
<Tooltip
description={tooltip}
align="center"
side="right"
sideOffset={10}
delayDuration={0}
>
<button
className={buttonClasses}
data-testid={view}
onClick={() => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
}}
>
<VegaIcon name={icon} size={20} />
</button>
</Tooltip>
);
};
const SidebarDivider = () => {
return (
<div
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-4 h-px"
role="separator"
/>
);
};
export const SidebarContent = () => {
const params = useParams();
const { view, setView } = useSidebar();
if (!view) return null;
if (view.type === ViewType.Order) {
if (params.marketId) {
return (
<ContentWrapper>
<DealTicketContainer
marketId={params.marketId}
onDeposit={(assetId) =>
setView({ type: ViewType.Deposit, assetId })
}
/>
</ContentWrapper>
);
} else {
return <CloseSidebar />;
}
}
if (view.type === ViewType.Info) {
if (params.marketId) {
return (
<ContentWrapper>
<MarketInfoAccordionContainer marketId={params.marketId} />
</ContentWrapper>
);
} else {
return <CloseSidebar />;
}
}
if (view.type === ViewType.Deposit) {
return (
<ContentWrapper title={t('Deposit')}>
<DepositContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Withdraw) {
return (
<ContentWrapper title={t('Withdraw')}>
<WithdrawContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Transfer) {
return (
<ContentWrapper title={t('Transfer')}>
<TransferContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Settings) {
return (
<ContentWrapper title={t('Settings')}>
<Settings />
</ContentWrapper>
);
}
throw new Error('invalid sidebar');
};
const ContentWrapper = ({
children,
title,
}: {
children: ReactNode;
title?: string;
}) => {
return (
<TinyScroll
className="h-full overflow-auto py-4 pl-3 pr-4"
// panes have p-1, since sidebar is on the right make pl less to account for additional pane space
data-testid="sidebar-content"
>
{title && <h2 className="mb-4">{title}</h2>}
{children}
</TinyScroll>
);
};
/** If rendered will close sidebar */
const CloseSidebar = () => {
const setView = useSidebar((store) => store.setView);
useEffect(() => {
setView(null);
}, [setView]);
return null;
};
export const useSidebar = create<{
init: boolean;
view: SidebarView | null;
setView: (view: SidebarView | null) => void;
}>()(
persist(
(set) => ({
init: true,
view: null,
setView: (x) =>
set(() => {
if (x === null) {
return { view: null, init: false };
}
return { view: x, init: false };
}),
}),
{
name: STORAGE_KEY,
}
)
);
-1
View File
@@ -1 +0,0 @@
export * from './tooltip';
@@ -1,72 +0,0 @@
import type { ReactNode } from 'react';
import React from 'react';
import {
Provider,
Root,
Trigger,
Content,
Portal,
Arrow,
} from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community';
const tooltipContentClasses =
'max-w-sm bg-vega-clight-500 dark:bg-vega-cdark-500 px-2 py-1 z-20 rounded text-default break-word';
export interface TooltipProps {
children: React.ReactElement;
description?: string | ReactNode;
open?: boolean;
align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
delayDuration?: number;
}
// Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({
children,
description,
open,
sideOffset,
align = 'start',
side = 'bottom',
delayDuration = 200,
}: TooltipProps) =>
description ? (
<Provider delayDuration={delayDuration} skipDelayDuration={100}>
<Root open={open}>
<Trigger
asChild
className="underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed"
>
{children}
</Trigger>
{description && (
<Portal>
<Content
align={align}
side={side}
alignOffset={8}
className={tooltipContentClasses}
sideOffset={sideOffset}
>
<div className="relative z-0" data-testid="tooltip-content">
{description}
</div>
<Arrow
width={16}
height={8}
className="fill-vega-clight-500 dark:fill-vega-cdark-500"
/>
</Content>
</Portal>
)}
</Root>
</Provider>
) : (
children
);
export const TooltipCellComponent = (props: ITooltipParams) => {
return <p className={tooltipContentClasses}>{props.value}</p>;
};
@@ -21,8 +21,8 @@ import type { PubKey } from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { WalletIcon } from '../icons/wallet';
import { useTransferDialog } from '@vegaprotocol/accounts';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
import { ViewType, useSidebar } from '../sidebar';
const MobileWalletButton = ({
isConnected,
@@ -35,7 +35,7 @@ const MobileWalletButton = ({
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const setView = useSidebar((store) => store.setView);
const openTransferDialog = useTransferDialog((store) => store.open);
const { VEGA_ENV } = useEnvironment();
const isYellow = VEGA_ENV === Networks.TESTNET;
const [drawerOpen, setDrawerOpen] = useState(false);
@@ -128,7 +128,7 @@ const MobileWalletButton = ({
<Button
onClick={() => {
setDrawerOpen(false);
setView({ type: ViewType.Transfer });
openTransferDialog(true);
}}
fill
>
@@ -149,7 +149,7 @@ export const VegaWalletConnectButton = () => {
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const setView = useSidebar((store) => store.setView);
const openTransferDialog = useTransferDialog((store) => store.open);
const {
pubKey,
pubKeys,
@@ -190,10 +190,6 @@ export const VegaWalletConnectButton = () => {
>
<DropdownMenuContent
onInteractOutside={() => setDropdownOpen(false)}
sideOffset={17}
side="bottom"
align="end"
onEscapeKeyDown={() => setDropdownOpen(false)}
>
<div className="min-w-[340px]" data-testid="keypair-list">
<DropdownMenuRadioGroup
@@ -210,10 +206,7 @@ export const VegaWalletConnectButton = () => {
{!isReadOnly && (
<DropdownMenuItem
data-testid="wallet-transfer"
onClick={() => {
setView({ type: ViewType.Transfer });
setDropdownOpen(false);
}}
onClick={() => openTransferDialog(true)}
>
{t('Transfer')}
</DropdownMenuItem>
@@ -267,7 +260,9 @@ const KeypairItem = ({ pk }: { pk: PubKey }) => {
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
{copied && (
<span className="text-xs text-neutral-500">{t('Copied')}</span>
)}
</span>
</div>
<DropdownMenuItemIndicator />
@@ -308,7 +303,9 @@ const KeypairListItem = ({
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
{copied && (
<span className="text-xs text-neutral-500">{t('Copied')}</span>
)}
</span>
</div>
);
@@ -44,7 +44,7 @@ export const ProposedMarkets = () => {
const tokenLink = useLinks(DApp.Token);
return useMemo(
() => (
<div className="mt-7 pt-8 border-t border-default">
<div className="mt-7 pt-8 border-t border-neutral-700">
{newMarkets.length > 0 ? (
<>
<h2 className="font-alpha uppercase text-2xl">
@@ -1,7 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Routes } from '../../pages/client-router';
import { Link } from 'react-router-dom';
import {
ExternalLink,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
export const RiskMessage = () => {
return (
@@ -27,12 +30,15 @@ export const RiskMessage = () => {
{t(
'By using the Vega Console, you acknowledge that you have read and understood the'
)}{' '}
<Link className="underline" to={Routes.DISCLAIMER} target="_blank">
<ExternalLink
href={`/#/${Links[Routes.DISCLAIMER]()}`}
className="underline"
>
<span className="flex items-center gap-1">
<span>{t('Vega Console Disclaimer')}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</span>
</Link>
</ExternalLink>
</p>
</>
);
@@ -1 +0,0 @@
export * from './withdraw-container';
@@ -1,27 +0,0 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import { WithdrawFormContainer } from '@vegaprotocol/withdraws';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
export const WithdrawContainer = ({ assetId }: { assetId?: string }) => {
const { pubKey } = useVegaWallet();
const createTransaction = useVegaTransactionStore((state) => state.create);
return (
<WithdrawFormContainer
assetId={assetId}
partyId={pubKey ? pubKey : undefined}
submit={({ amount, asset, receiverAddress }) => {
createTransaction({
withdrawSubmission: {
amount,
asset,
ext: {
erc20: {
receiverAddress,
},
},
},
});
}}
/>
);
};
@@ -7,7 +7,6 @@ export const useMarketClickHandler = (replace = false) => {
const { marketId } = useParams();
const { pathname } = useLocation();
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.MARKET](selectedId);
+12 -6
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import Head from 'next/head';
import type { AppProps } from 'next/app';
import { t } from '@vegaprotocol/i18n';
@@ -16,6 +17,7 @@ import {
} from '@vegaprotocol/web3';
import {
envTriggerMapping,
Networks,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
@@ -23,22 +25,22 @@ import {
} from '@vegaprotocol/environment';
import './styles.css';
import { usePageTitleStore } from '../stores';
import { Footer } from '../components/footer';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { ViewingBanner } from '../components/viewing-banner';
import { AnnouncementBanner, UpgradeBanner } from '../components/banner';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import { AnnouncementBanner, UpgradeBanner } from '../components/banner';
import { Navbar } from '../components/navbar';
import classNames from 'classnames';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -74,12 +76,15 @@ const InitializeHandlers = () => {
function AppBody({ Component }: AppProps) {
const location = useLocation();
const { VEGA_ENV } = useEnvironment();
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
);
return (
<div className="h-full overflow-hidden">
<div className="h-full dark:bg-black dark:text-white">
<Head>
{/* Cannot use meta tags in _document.page.tsx see https://nextjs.org/docs/messages/no-document-viewport-meta */}
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -87,7 +92,7 @@ function AppBody({ Component }: AppProps) {
<Title />
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme="system" />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
@@ -98,6 +103,7 @@ function AppBody({ Component }: AppProps) {
<main data-testid={location.pathname}>
<Component />
</main>
<Footer />
</div>
<DialogsContainer />
<ToastsManager />
+1 -1
View File
@@ -21,7 +21,7 @@ export default function Document() {
/>
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
<body className="font-alpha dark:bg-black dark:text-white">
<Main />
<NextScript />
</body>
+50 -40
View File
@@ -1,11 +1,10 @@
import { Suspense } from 'react';
import type { RouteObject } from 'react-router-dom';
import { Outlet, useRoutes } from 'react-router-dom';
import { useRoutes } from 'react-router-dom';
import dynamic from 'next/dynamic';
import { t } from '@vegaprotocol/i18n';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import trimEnd from 'lodash/trimEnd';
import { LayoutWithSidebar } from '../components/layouts';
const LazyHome = dynamic(() => import('../client-pages/home'), {
ssr: false,
@@ -27,72 +26,83 @@ const LazyPortfolio = dynamic(() => import('../client-pages/portfolio'), {
ssr: false,
});
const LazySettings = dynamic(() => import('../client-pages/settings'), {
ssr: false,
});
const LazyDisclaimer = dynamic(() => import('../client-pages/disclaimer'), {
ssr: false,
});
export enum Routes {
HOME = '/',
MARKET = '/markets/:marketId',
MARKET = '/markets',
MARKETS = '/markets/all',
PORTFOLIO = '/portfolio',
LIQUIDITY = '/liquidity/:marketId',
DISCLAIMER = '/disclaimer',
LIQUIDITY = 'liquidity/:marketId',
SETTINGS = 'settings',
DISCLAIMER = 'disclaimer',
}
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
export const Links: ConsoleLinks = {
[Routes.HOME]: () => Routes.HOME,
[Routes.MARKET]: (marketId: string) =>
trimEnd(Routes.MARKET.replace(':marketId', marketId)),
[Routes.MARKET]: (marketId: string | null | undefined) =>
marketId ? trimEnd(`${Routes.MARKET}/${marketId}`, '/') : Routes.MARKET,
[Routes.MARKETS]: () => Routes.MARKETS,
[Routes.PORTFOLIO]: () => Routes.PORTFOLIO,
[Routes.LIQUIDITY]: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
[Routes.LIQUIDITY]: (marketId: string | null | undefined) =>
marketId
? trimEnd(`${Routes.LIQUIDITY}/${marketId}`, '/')
: Routes.LIQUIDITY,
[Routes.SETTINGS]: () => Routes.SETTINGS,
[Routes.DISCLAIMER]: () => Routes.DISCLAIMER,
};
const routerConfig: RouteObject[] = [
{
path: '/*',
element: <LayoutWithSidebar />,
index: true,
element: <LazyHome />,
},
{
path: Routes.MARKETS,
element: <LazyMarkets />,
},
{
path: Routes.MARKET,
children: [
// all pages that require the Layout component (Sidebar)
{
index: true,
element: <LazyHome />,
element: <LazyMarket />,
},
{
path: 'markets',
element: <Outlet />,
children: [
{
path: 'all',
element: <LazyMarkets />,
},
{
path: ':marketId',
element: <LazyMarket />,
},
],
},
{
path: 'portfolio',
element: <LazyPortfolio />,
},
{
path: 'liquidity',
element: <Outlet />,
children: [
{
path: ':marketId',
element: <LazyLiquidity />,
},
],
path: ':marketId',
element: <LazyMarket />,
},
],
},
{
path: Routes.LIQUIDITY,
element: <LazyLiquidity />,
children: [
{
index: true,
element: <LazyLiquidity />,
},
{
path: ':marketId',
element: <LazyLiquidity />,
},
],
},
{
path: Routes.PORTFOLIO,
element: <LazyPortfolio />,
},
{
path: Routes.SETTINGS,
element: <LazySettings />,
},
{
path: Routes.DISCLAIMER,
element: <LazyDisclaimer />,
+6
View File
@@ -4,11 +4,14 @@ import {
} from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws';
import { DepositDialog } from '@vegaprotocol/deposits';
import {
Web3ConnectUncontrolledDialog,
WithdrawalApprovalDialogContainer,
} from '@vegaprotocol/web3';
import { WelcomeDialog } from '../components/welcome-dialog';
import { TransferDialog } from '@vegaprotocol/accounts';
import { RiskMessage } from '../components/welcome-dialog';
const DialogsContainer = () => {
@@ -26,7 +29,10 @@ const DialogsContainer = () => {
onChange={setOpen}
/>
<WelcomeDialog />
<DepositDialog />
<Web3ConnectUncontrolledDialog />
<CreateWithdrawalDialog />
<TransferDialog />
<WithdrawalApprovalDialogContainer />
</>
);
+26 -74
View File
@@ -5,58 +5,29 @@
@tailwind components;
@tailwind utilities;
/**
* TAILWIND HELPERS
*/
html,
body,
#__next {
@apply h-full;
}
.text-default {
@apply text-vega-clight-50 dark:text-vega-cdark-50;
}
.text-secondary {
@apply text-vega-clight-100 dark:text-vega-cdark-100;
}
.text-muted {
@apply text-vega-clight-200 dark:text-vega-cdark-200;
}
.border-default {
@apply border-vega-clight-600 dark:border-vega-cdark-600;
}
/**
* ALLOTMENT
*/
/* Styles for allotment */
html {
--focus-border: theme(colors.vega.pink.500);
--pennant-color-danger: theme(colors.vega.pink.500);
--focus-border: theme('colors.vega.pink.500');
--separator-border: theme('colors.vega.light.200');
--pennant-color-danger: theme('colors.vega.pink.500');
}
html.dark {
--focus-border: theme(colors.vega.yellow.500);
--focus-border: theme('colors.vega.yellow.500');
--separator-border: theme('colors.vega.dark.200');
}
/* hide pane separation border, we leave it blank so border is applied within a padded area */
.split-view-view::before {
display: none;
.border-default {
@apply border-vega-light-200 dark:border-vega-dark-200;
}
/* re show separator border within chart */
.plot-container__chart .split-view-view::before {
display: block;
}
/**
* PENNANT
*/
/* PENNANT */
html [data-theme='dark'],
html [data-theme='light'] {
@@ -76,9 +47,6 @@ html [data-theme='light'] {
}
html [data-theme='light'] {
--separator-border: theme(colors.vega.clight.400);
--pennant-background-surface-color: theme(colors.vega.clight.900);
/* candles */
--pennant-color-buy-fill: theme(colors.market.green.500);
--pennant-color-buy-stroke: theme(colors.market.green.600);
@@ -97,9 +65,6 @@ html [data-theme='light'] {
}
html [data-theme='dark'] {
--separator-border: theme(colors.vega.cdark.400);
--pennant-background-surface-color: theme('colors.vega.cdark.900');
/* candles */
--pennant-color-buy-fill: theme(colors.market.green.600);
--pennant-color-buy-stroke: theme(colors.market.green.500);
@@ -117,11 +82,7 @@ html [data-theme='dark'] {
--pennant-color-volume-sell: theme(colors.market.red.600);
}
/**
* AG GRID
*
* - Do not edit without updating other global stylesheets for each app
*/
/* AG GRID - Do not edit without updating other global stylesheets for each app */
.vega-ag-grid .ag-root-wrapper {
border: solid 0px;
@@ -142,32 +103,26 @@ html [data-theme='dark'] {
border-width: 0;
}
.vega-ag-grid .ag-header-row {
@apply font-alpha font-normal;
}
/* Light variables */
.ag-theme-balham {
--ag-background-color: theme(colors.white);
--ag-border-color: theme(colors.vega.clight.600);
--ag-header-background-color: theme(colors.vega.clight.700);
--ag-border-color: theme(colors.neutral[300]);
--ag-header-background-color: theme(colors.white);
--ag-odd-row-background-color: theme(colors.white);
--ag-header-column-separator-color: theme(colors.vega.clight.500);
--ag-row-border-color: theme(colors.vega.clight.600);
--ag-row-hover-color: theme(colors.vega.clight.800);
--ag-modal-overlay-background-color: rgb(244 244 244 / 50%);
--ag-header-column-separator-color: theme(colors.neutral[300]);
--ag-row-border-color: theme(colors.white);
--ag-row-hover-color: theme(colors.neutral[100]);
}
/* Dark variables */
.ag-theme-balham-dark {
--ag-background-color: theme(colors.vega.cdark.900);
--ag-border-color: theme(colors.vega.cdark.600);
--ag-header-background-color: theme(colors.vega.cdark.700);
--ag-odd-row-background-color: theme(colors.vega.cdark.900);
--ag-header-column-separator-color: theme(colors.vega.cdark.500);
--ag-row-border-color: theme(colors.vega.cdark.600);
--ag-row-hover-color: theme(colors.vega.cdark.800);
--ag-modal-overlay-background-color: rgb(9 11 16 / 50%);
--ag-background-color: theme(colors.black);
--ag-border-color: theme(colors.neutral[700]);
--ag-header-background-color: theme(colors.black);
--ag-odd-row-background-color: theme(colors.black);
--ag-header-column-separator-color: theme(colors.neutral[600]);
--ag-row-border-color: theme(colors.black);
--ag-row-hover-color: theme(colors.neutral[800]);
}
.ag-theme-balham-dark .ag-row.no-hover,
.ag-theme-balham-dark .ag-row.no-hover:hover,
@@ -176,26 +131,23 @@ html [data-theme='dark'] {
background: var(--ag-background-color);
}
/**
* REACT VIRTUALIZED list
*/
.vega-scrollbar {
.virtualized-list {
/* Works on Firefox */
scrollbar-width: thin;
scrollbar-color: #999 #333;
}
/* Works on Chrome, Edge, and Safari */
.vega-scrollbar::-webkit-scrollbar {
.virtualized-list::-webkit-scrollbar {
width: 6px;
background-color: #999;
}
.vega-scrollbar::-webkit-scrollbar-thumb {
.virtualized-list::-webkit-scrollbar-thumb {
width: 6px;
background-color: #333;
}
.vega-scrollbar::-webkit-scrollbar-track {
.virtualized-list::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}
@@ -8,6 +8,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useTransferDialog } from './transfer-dialog';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
export const AccountsActionsDropdown = ({
@@ -16,16 +17,15 @@ export const AccountsActionsDropdown = ({
onClickDeposit,
onClickWithdraw,
onClickBreakdown,
onClickTransfer,
}: {
assetId: string;
assetContractAddress?: string;
onClickDeposit: () => void;
onClickWithdraw: () => void;
onClickBreakdown: () => void;
onClickTransfer: () => void;
}) => {
const etherscanLink = useEtherscanLink();
const openTransferDialog = useTransferDialog((store) => store.open);
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
return (
@@ -49,7 +49,7 @@ export const AccountsActionsDropdown = ({
<DropdownMenuItem
key={'transfer'}
data-testid="transfer"
onClick={onClickTransfer}
onClick={() => openTransferDialog(true, assetId)}
>
<VegaIcon name={VegaIconNames.TRANSFER} size={16} />
{t('Transfer')}
@@ -100,7 +100,6 @@ interface AccountManagerProps {
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
onClickTransfer?: (assetId?: string) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
@@ -111,7 +110,6 @@ export const AccountManager = ({
onClickAsset,
onClickWithdraw,
onClickDeposit,
onClickTransfer,
partyId,
isReadOnly,
pinnedAsset,
@@ -143,7 +141,6 @@ export const AccountManager = ({
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
onClickTransfer={onClickTransfer}
onClickBreakdown={setBreakdownAssetId}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
+3 -8
View File
@@ -31,6 +31,7 @@ import { AccountsActionsDropdown } from './accounts-actions-dropdown';
const colorClass = (percentageUsed: number, neutral = false) => {
return classNames('text-right', {
'text-neutral-500 dark:text-neutral-400': percentageUsed < 75 && !neutral,
'text-vega-orange': percentageUsed >= 75 && percentageUsed < 90,
'text-vega-red': percentageUsed >= 90,
});
@@ -70,7 +71,6 @@ export interface AccountTableProps extends AgGridReactProps {
onClickWithdraw?: (assetId: string) => void;
onClickDeposit?: (assetId: string) => void;
onClickBreakdown?: (assetId: string) => void;
onClickTransfer?: (assetId: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
}
@@ -82,7 +82,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
onClickWithdraw,
onClickDeposit,
onClickBreakdown,
onClickTransfer,
rowData,
isReadOnly,
pinnedAsset,
@@ -184,8 +183,8 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
<span className="ml-2 inline-block w-14 text-vega-light-200 dark:text-vega-dark-200">
{t('0.00%')}'
</span>
</>
);
@@ -286,9 +285,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
@@ -300,7 +296,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
+1 -1
View File
@@ -7,7 +7,7 @@ export * from './breakdown-table';
export * from './use-account-balance';
export * from './get-settlement-account';
export * from './use-market-account-balance';
export * from './transfer-dialog';
export * from './__generated__/Margins';
export { MarginHealthChart } from './margin-health-chart';
export * from './margin-data-provider';
export * from './transfer-container';
+10 -50
View File
@@ -7,42 +7,31 @@ import {
} from '@vegaprotocol/network-parameters';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Transfer } from '@vegaprotocol/wallet';
import {
useVegaTransactionStore,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet';
import { useCallback, useMemo } from 'react';
import { accountsDataProvider } from './accounts-data-provider';
import { TransferForm } from './transfer-form';
import { useTransferDialog } from './transfer-dialog';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import sortBy from 'lodash/sortBy';
import {
ExternalLink,
Intent,
Lozenge,
Notification,
} from '@vegaprotocol/ui-toolkit';
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const { pubKey, pubKeys } = useVegaWallet();
const open = useTransferDialog((store) => store.open);
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
const { data } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const create = useVegaTransactionStore((store) => store.create);
const transfer = useCallback(
(transfer: Transfer) => {
create({ transfer });
open(false);
},
[create]
[create, open]
);
const assets = useMemo(() => {
@@ -62,40 +51,11 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
return (
<>
<p className="text-sm mb-4" data-testid="transfer-intro-text">
{t('Transfer funds to another Vega key')}
{pubKey && (
<>
{t(' from ')}
<Lozenge className="font-mono">
{truncateByChars(pubKey || '')}
</Lozenge>
</>
)}
{t('. If you are at all unsure, stop and seek advice.')}
<p className="text-sm mb-4" data-testid="dialog-transfer-text">
{t('Transfer funds to another Vega key from')}{' '}
<Lozenge className="font-mono">{truncateByChars(pubKey || '')}</Lozenge>{' '}
{t('If you are at all unsure, stop and seek advice.')}
</p>
{!pubKey && (
<div className="mb-4">
<Notification
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
to make a transfer.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'sm',
}}
/>
</div>
)}
<TransferForm
pubKey={pubKey}
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
+30
View File
@@ -0,0 +1,30 @@
import { t } from '@vegaprotocol/i18n';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { TransferContainer } from './transfer-container';
interface State {
isOpen: boolean;
assetId: string | undefined;
}
interface Actions {
open: (open?: boolean, assetId?: string) => void;
}
export const useTransferDialog = create<State & Actions>()((set) => ({
isOpen: false,
assetId: undefined,
open: (open = true, assetId) => {
set(() => ({ isOpen: open, assetId }));
},
}));
export const TransferDialog = () => {
const { isOpen, open, assetId } = useTransferDialog();
return (
<Dialog title={t('Transfer')} open={isOpen} onChange={open} size="small">
<TransferContainer assetId={assetId} />
</Dialog>
);
};

Some files were not shown because too many files have changed in this diff Show More