Search implementation (#97)
* make titles look prettier * link to parties from transactions table * render what data is found conditionally * more syntax highlighting * re-jig file orders * remove footer component for now * add subheading component * adjust column layout * Style up header * enable ligatures * change env files * fix error if data is null * show governance header even if there is no data * remove dead css * add dark theme for block explorer * use memo on parties submit * remove some css from header * basic search implementation * allow passing classNames to form group * add tests for form group * add form-grpup stories * bad rebase fixes * add link * tidy up tests * fix tests * tidy up env files * final test fixes * switch order of classes * fix test id * force build for testing * rename file for linting * add tests for header component * rename export * input error tests * use descriptive function names as per PR comment * fix casing issue for CI * handle empty state * make query easier to understand
This commit is contained in:
parent
9ced1ce0fd
commit
ae37f76b1c
@ -1,23 +1,3 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n04.d.vega.xyz/tm"
|
||||
|
@ -1,23 +1,3 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz/"
|
||||
@ -28,7 +8,6 @@ NX_VEGA_URL = "https://api.token.vega.xyz/query"
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -1,30 +1,9 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n03.s.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.s.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
|
||||
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
|
@ -1,23 +1,3 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n03.stagnet2.vega.xyz/tm"
|
||||
|
@ -1,23 +1,3 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
|
@ -7,7 +7,7 @@ export default class NetworkParametersPage extends BasePage {
|
||||
verifyNetworkParametersDisplayed() {
|
||||
cy.getByTestId(this.networkParametersHeader).should(
|
||||
'have.text',
|
||||
'NetworkParameters'
|
||||
'Network Parameters'
|
||||
);
|
||||
cy.getByTestId(this.parameters).should('not.be.empty');
|
||||
}
|
||||
|
@ -77,6 +77,6 @@ export default class TransactionsPage extends BasePage {
|
||||
}
|
||||
|
||||
clickOnTopTransaction() {
|
||||
cy.getByTestId(this.transactionRow).first().find('a').click();
|
||||
cy.getByTestId(this.transactionRow).first().find('a').first().click();
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,10 @@ NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -1,34 +1,5 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n04.d.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://n04.d.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://n04.d.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
14
apps/explorer/.env.local
Normal file
14
apps/explorer/.env.local
Normal file
@ -0,0 +1,14 @@
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
@ -1,34 +1,5 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz/"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://mainnet-observer-proxy01.ops.vega.xyz/websocket"
|
||||
NX_VEGA_URL = "https://api.token.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -1,35 +1,5 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n03.s.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.s.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
|
||||
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -1,34 +1,5 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://n03.stagnet2.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://n03.stagnet2.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://n03.stagnet2.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -1,34 +1,5 @@
|
||||
# React Environment Variables
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
|
@ -7,5 +7,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/explorer',
|
||||
setupFilesAfterEnv: ['./src/app/setupTests.ts'],
|
||||
setupFilesAfterEnv: ['./src/app/setup-tests.ts'],
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
@import './styles/colors';
|
||||
@import './styles/fonts';
|
||||
@import './styles/reset';
|
||||
|
||||
html,
|
||||
body,
|
||||
@ -39,18 +38,23 @@ body,
|
||||
padding: 20px;
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 1;
|
||||
grid-row-start: 1;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid $white;
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 2;
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
grid-row-start: 1;
|
||||
grid-row-end: 2;
|
||||
|
||||
h1 {
|
||||
font-family: $font-alpa-lyrae;
|
||||
font-feature-settings: 'calt';
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import App from './App';
|
||||
import App from './app';
|
||||
|
||||
describe('App', () => {
|
||||
it('should exist', () => {
|
||||
|
@ -4,7 +4,6 @@ import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
import { createClient } from './lib/apollo-client';
|
||||
import { Nav } from './components/nav';
|
||||
import { Footer } from './components/footer';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import React from 'react';
|
||||
@ -24,7 +23,6 @@ function App() {
|
||||
<Nav />
|
||||
<Header />
|
||||
<Main />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</ApolloProvider>
|
||||
|
@ -1,15 +0,0 @@
|
||||
// import packageJson from "../../../package.json";
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer>
|
||||
<section>
|
||||
<div>Reading Vega Fairground data from </div>
|
||||
<div>
|
||||
{/* Version/commit hash: {packageJson.version} / */}
|
||||
{process.env['NX_COMMIT_REF'] || 'dev'}
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
);
|
||||
};
|
28
apps/explorer/src/app/components/header/header.spec.tsx
Normal file
28
apps/explorer/src/app/components/header/header.spec.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Header } from './header';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
jest.mock('../search', () => ({
|
||||
Search: () => <div data-testid="search">OrderList</div>,
|
||||
}));
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('Header', () => {
|
||||
it('should render heading', () => {
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
|
||||
'Vega Explorer'
|
||||
);
|
||||
});
|
||||
it('should render search', () => {
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
});
|
||||
});
|
12
apps/explorer/src/app/components/header/header.tsx
Normal file
12
apps/explorer/src/app/components/header/header.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Search } from '../search';
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className="flex px-16 pt-16 pb-8">
|
||||
<h1 className="text-h3" data-testid="explorer-header">
|
||||
Vega Explorer
|
||||
</h1>
|
||||
<Search />
|
||||
</header>
|
||||
);
|
||||
};
|
@ -1,9 +1 @@
|
||||
import Search from "../search";
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header>
|
||||
<Search />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
export * from './header';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../routes/router-config';
|
||||
|
||||
export const JumpToBlock = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -14,7 +15,7 @@ export const JumpToBlock = () => {
|
||||
const blockNumber = target.blockNumber.value;
|
||||
|
||||
if (blockNumber) {
|
||||
navigate(`/blocks/${blockNumber}`);
|
||||
navigate(`/${Routes.BLOCKS}/${blockNumber}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -28,15 +29,15 @@ export const JumpToBlock = () => {
|
||||
</label>
|
||||
<input
|
||||
id="block-input"
|
||||
type="tel"
|
||||
name={'blockNumber'}
|
||||
placeholder={'Block number'}
|
||||
type="number"
|
||||
name="blockNumber"
|
||||
placeholder="Block number"
|
||||
className="bg-white-25 border-white border px-8 py-4 placeholder-white-60"
|
||||
/>
|
||||
<input
|
||||
className="border-white border px-28 py-4 cursor-pointer"
|
||||
type={'submit'}
|
||||
value={'Go'}
|
||||
type="submit"
|
||||
value="Go"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
@ -1,12 +1,16 @@
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
interface RouteTitleProps {
|
||||
interface RouteTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RouteTitle = ({ children, className }: RouteTitleProps) => {
|
||||
export const RouteTitle = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: RouteTitleProps) => {
|
||||
const classes = classnames(
|
||||
'font-alpha',
|
||||
'text-h3',
|
||||
@ -15,5 +19,9 @@ export const RouteTitle = ({ children, className }: RouteTitleProps) => {
|
||||
'mb-28',
|
||||
className
|
||||
);
|
||||
return <h1 className={classes}>{children}</h1>;
|
||||
return (
|
||||
<h1 className={classes} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
|
@ -1,108 +1 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import debounce from 'lodash.debounce';
|
||||
import { Guess, GuessVariables } from '@vegaprotocol/graphql';
|
||||
|
||||
const TX_LENGTH = 64;
|
||||
|
||||
enum PossibleIdTypes {
|
||||
Block = 'Block',
|
||||
Tx = 'Tx',
|
||||
Party = 'Party',
|
||||
Market = 'Market',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
const GUESS_QUERY = gql`
|
||||
query Guess($guess: ID!) {
|
||||
party(id: $guess) {
|
||||
id
|
||||
}
|
||||
market(id: $guess) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const usePossibleType = (search: string) => {
|
||||
const [possibleType, setPossibleType] = useState<PossibleIdTypes>();
|
||||
const { data, loading, error } = useQuery<Guess, GuessVariables>(
|
||||
GUESS_QUERY,
|
||||
{
|
||||
variables: {
|
||||
guess: search,
|
||||
},
|
||||
skip: !search,
|
||||
}
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isNaN(Number(search))) {
|
||||
setPossibleType(PossibleIdTypes.Block);
|
||||
} else if (data?.party) {
|
||||
setPossibleType(PossibleIdTypes.Party);
|
||||
} else if (data?.market) {
|
||||
setPossibleType(PossibleIdTypes.Market);
|
||||
} else if (search.replace('0x', '').length === TX_LENGTH) {
|
||||
setPossibleType(PossibleIdTypes.Tx);
|
||||
} else {
|
||||
setPossibleType(PossibleIdTypes.Unknown);
|
||||
}
|
||||
}, [data?.market, data?.party, search, setPossibleType]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
possibleType,
|
||||
};
|
||||
};
|
||||
|
||||
const useGuess = () => {
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState<string>('');
|
||||
const { loading, error, possibleType } = usePossibleType(debouncedSearch);
|
||||
const debouncedSearchSet = React.useMemo(
|
||||
() => debounce(setDebouncedSearch, 1000),
|
||||
[setDebouncedSearch]
|
||||
);
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const search = event.target.value;
|
||||
setSearch(search);
|
||||
debouncedSearchSet(search);
|
||||
},
|
||||
[debouncedSearchSet]
|
||||
);
|
||||
|
||||
return {
|
||||
onChange,
|
||||
search,
|
||||
loading,
|
||||
error,
|
||||
possibleType,
|
||||
};
|
||||
};
|
||||
|
||||
const Search = () => {
|
||||
const { search, onChange } = useGuess();
|
||||
return (
|
||||
<section>
|
||||
<h1 data-testid="explorer-header">Vega Block Explorer</h1>
|
||||
<fieldset>
|
||||
<label htmlFor="search">Search: </label>
|
||||
<input
|
||||
data-testid="search-field"
|
||||
name="search"
|
||||
value={search}
|
||||
onChange={(e) => onChange(e)}
|
||||
/>
|
||||
</fieldset>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
export * from './search';
|
||||
|
140
apps/explorer/src/app/components/search/search.spec.tsx
Normal file
140
apps/explorer/src/app/components/search/search.spec.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { Search } from './search';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Routes } from '../../routes/router-config';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mockedNavigate.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Search />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const getInputs = () => ({
|
||||
input: screen.getByTestId('search'),
|
||||
button: screen.getByTestId('search-button'),
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
it('should render search input and button', () => {
|
||||
render(renderComponent());
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('search-button')).toHaveTextContent('Search');
|
||||
});
|
||||
|
||||
it('should render error if input is not known', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, { target: { value: 'asd' } });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
"Something doesn't look right"
|
||||
);
|
||||
});
|
||||
it('should render error if no input is given', async () => {
|
||||
render(renderComponent());
|
||||
const { button } = getInputs();
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Search required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render error if transaction is not hex', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x123456789012345678901234567890123456789012345678901234567890123Q',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Transaction is not hexadecimal'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render error if transaction is not hex and does not have leading 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'123456789012345678901234567890123456789012345678901234567890123Q',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Transaction is not hexadecimal'
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to transactions page', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to transactions page without proceeding 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to blocks page if passed a number', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '123',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(`${Routes.BLOCKS}/123`);
|
||||
});
|
||||
});
|
||||
});
|
84
apps/explorer/src/app/components/search/search.tsx
Normal file
84
apps/explorer/src/app/components/search/search.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { FormGroup, Input, InputError, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../routes/router-config';
|
||||
|
||||
const TX_LENGTH = 64;
|
||||
|
||||
interface FormFields {
|
||||
search: string;
|
||||
}
|
||||
|
||||
const isPrependedTransaction = (search: string) =>
|
||||
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
|
||||
|
||||
const isTransaction = (search: string) =>
|
||||
!search.startsWith('0x') && search.length === TX_LENGTH;
|
||||
|
||||
const isBlock = (search: string) => !Number.isNaN(Number(search));
|
||||
|
||||
export const Search = () => {
|
||||
const { register, handleSubmit } = useForm<FormFields>();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
const onSubmit = React.useCallback(
|
||||
(fields: FormFields) => {
|
||||
setError(null);
|
||||
|
||||
const search = fields.search;
|
||||
if (!search) {
|
||||
setError(new Error('Search required'));
|
||||
} else if (isPrependedTransaction(search)) {
|
||||
if (Number.isNaN(Number(search))) {
|
||||
setError(new Error('Transaction is not hexadecimal'));
|
||||
} else {
|
||||
navigate(`${Routes.TX}/${search}`);
|
||||
}
|
||||
} else if (isTransaction(search)) {
|
||||
if (Number.isNaN(Number(`0x${search}`))) {
|
||||
setError(new Error('Transaction is not hexadecimal'));
|
||||
} else {
|
||||
navigate(`${Routes.TX}/0x${search}`);
|
||||
}
|
||||
} else if (isBlock(search)) {
|
||||
navigate(`${Routes.BLOCKS}/${Number(search)}`);
|
||||
} else {
|
||||
setError(new Error("Something doesn't look right"));
|
||||
}
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex-1 flex ml-16 justify-end"
|
||||
>
|
||||
<FormGroup className="w-2/3 mb-0">
|
||||
<Input
|
||||
{...register('search')}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder="Enter block number or transaction hash"
|
||||
/>
|
||||
{error?.message ? (
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="flex-1 w-full"
|
||||
>
|
||||
{error.message}
|
||||
</InputError>
|
||||
) : (
|
||||
<div className="h-28"></div>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button type="submit" variant="secondary" data-testid="search-button">
|
||||
Search
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
27
apps/explorer/src/app/components/sub-heading/index.tsx
Normal file
27
apps/explorer/src/app/components/sub-heading/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import classnames from 'classnames';
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
interface SubHeadingProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SubHeading = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: SubHeadingProps) => {
|
||||
const classes = classnames(
|
||||
'font-alpha',
|
||||
'text-h4',
|
||||
'uppercase',
|
||||
'mt-12',
|
||||
'mb-12',
|
||||
className
|
||||
);
|
||||
return (
|
||||
<h2 {...props} className={classes}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TendermintBlockchainResponse } from '../../../routes/blocks/tendermint-blockchain-response';
|
||||
import { BlockData } from '../../blocks';
|
||||
import { TxsPerBlock } from '../txs-per-block';
|
||||
import { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
|
||||
import { BlockData } from '../blocks';
|
||||
import { TxsPerBlock } from './txs-per-block';
|
||||
|
||||
interface TxsProps {
|
||||
data: TendermintBlockchainResponse | undefined;
|
@ -1,4 +1,3 @@
|
||||
export { TxDetails } from './id/tx-details';
|
||||
export { TxContent } from './id/tx-content';
|
||||
export { TxList } from './pending/tx-list';
|
||||
export { BlockTxsData } from './home/block-txs-data';
|
||||
export { TxList } from './tx-list';
|
||||
export { BlockTxsData } from './block-txs-data';
|
||||
export { TxOrderType } from './tx-order-type';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TendermintUnconfirmedTransactionsResponse } from '../../../routes/txs/tendermint-unconfirmed-transactions-response.d';
|
||||
import { TendermintUnconfirmedTransactionsResponse } from '../../routes/txs/tendermint-unconfirmed-transactions-response.d';
|
||||
|
||||
interface TxsProps {
|
||||
data: TendermintUnconfirmedTransactionsResponse | undefined;
|
@ -1,11 +1,11 @@
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
|
||||
import { Routes } from '../../../routes/router-config';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
|
||||
import { Routes } from '../../routes/router-config';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { RenderFetched } from '../../render-fetched';
|
||||
import { TruncateInline } from '../../truncate/truncate';
|
||||
import { TxOrderType } from '../tx-order-type';
|
||||
import { RenderFetched } from '../render-fetched';
|
||||
import { TruncateInline } from '../truncate/truncate';
|
||||
import { TxOrderType } from './tx-order-type';
|
||||
|
||||
interface TxsPerBlockProps {
|
||||
blockHeight: string | undefined;
|
||||
@ -31,6 +31,7 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
|
||||
|
||||
return (
|
||||
<RenderFetched error={error} loading={loading} className="text-body-large">
|
||||
{decodedBlockData && decodedBlockData.length ? (
|
||||
<div className="overflow-x-auto whitespace-nowrap mb-28">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
@ -41,10 +42,9 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{decodedBlockData &&
|
||||
decodedBlockData.map(({ TxHash, PubKey, Type }) => {
|
||||
{decodedBlockData.map(({ TxHash, PubKey, Type }) => {
|
||||
return (
|
||||
<tr data-testid="transaction-row" key={TxHash}>
|
||||
<tr key={TxHash} data-testid="transaction-row">
|
||||
<td>
|
||||
<Link to={`/${Routes.TX}/${TxHash}`}>
|
||||
<TruncateInline
|
||||
@ -56,12 +56,14 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/${Routes.PARTIES}/${PubKey}`}>
|
||||
<TruncateInline
|
||||
text={PubKey}
|
||||
startChars={truncateLength}
|
||||
endChars={truncateLength}
|
||||
className="font-mono"
|
||||
className="text-vega-yellow font-mono"
|
||||
/>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<TxOrderType className="mb-4" orderType={Type} />
|
||||
@ -72,6 +74,11 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-mono mb-28">
|
||||
No transactions in block {blockHeight}
|
||||
</div>
|
||||
)}
|
||||
</RenderFetched>
|
||||
);
|
||||
};
|
@ -57,7 +57,7 @@ function useFetch<T = unknown>(
|
||||
}
|
||||
|
||||
const data = (await response.json()) as T;
|
||||
if ('error' in data) {
|
||||
if (data && 'error' in data) {
|
||||
// @ts-ignore - data.error
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
import { AssetsQuery } from '@vegaprotocol/graphql';
|
||||
|
||||
@ -35,12 +37,12 @@ const Assets = () => {
|
||||
if (!data || !data.assets) return null;
|
||||
return (
|
||||
<section>
|
||||
<h1>Assets</h1>
|
||||
<RouteTitle data-testid="assets-header">Assets</RouteTitle>
|
||||
{data?.assets.map((a) => (
|
||||
<React.Fragment key={a.id}>
|
||||
<h2 data-testid="asset-header">
|
||||
<SubHeading data-testid="asset-header">
|
||||
{a.name} ({a.symbol})
|
||||
</h2>
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={a} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
@ -4,7 +4,6 @@ import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { TendermintBlocksResponse } from '../tendermint-blocks-response';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
||||
import { SecondsAgo } from '../../../components/seconds-ago';
|
||||
import {
|
||||
Table,
|
||||
@ -12,6 +11,7 @@ import {
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from '../../../components/table';
|
||||
import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
||||
|
||||
const Block = () => {
|
||||
const { block } = useParams<{ block: string }>();
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
@ -12,7 +13,7 @@ const Genesis = () => {
|
||||
if (!genesis?.result.genesis) return null;
|
||||
return (
|
||||
<section>
|
||||
<h1 data-testid="genesis-header">Genesis</h1>
|
||||
<RouteTitle data-testid="genesis-header">Genesis</RouteTitle>
|
||||
<SyntaxHighlighter data={genesis?.result.genesis} />
|
||||
</section>
|
||||
);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
import {
|
||||
ProposalsQuery,
|
||||
@ -100,14 +102,13 @@ const PROPOSAL_QUERY = gql`
|
||||
const Governance = () => {
|
||||
const { data } = useQuery<ProposalsQuery>(PROPOSAL_QUERY);
|
||||
|
||||
if (!data || !data.proposals) return null;
|
||||
if (!data) return null;
|
||||
return (
|
||||
<section>
|
||||
<h1>Governance</h1>
|
||||
{data.proposals.map((p) => (
|
||||
<RouteTitle data-testid="governance-header">Governance</RouteTitle>
|
||||
{data.proposals?.map((p) => (
|
||||
<React.Fragment key={p.id}>
|
||||
{/* TODO get proposal name generator from console */}
|
||||
<h2>{getProposalName(p.terms.change)}</h2>
|
||||
<SubHeading>{getProposalName(p.terms.change)}</SubHeading>
|
||||
<SyntaxHighlighter data={p} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
@ -3,6 +3,8 @@ import { MarketsQuery } from '@vegaprotocol/graphql';
|
||||
|
||||
import React from 'react';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
|
||||
const MARKETS_QUERY = gql`
|
||||
query MarketsQuery {
|
||||
@ -148,14 +150,17 @@ const Markets = () => {
|
||||
|
||||
if (!data || !data.markets) return null;
|
||||
return (
|
||||
<section className="px-8 py-12">
|
||||
<h1>Markets</h1>
|
||||
{data.markets.map((m) => (
|
||||
<section>
|
||||
<RouteTitle data-testid="markets-heading">Markets</RouteTitle>
|
||||
|
||||
{data
|
||||
? data.markets.map((m) => (
|
||||
<React.Fragment key={m.id}>
|
||||
<h2 data-testid="markets-header">{m.name}</h2>
|
||||
<SubHeading data-testid="markets-header">{m.name}</SubHeading>
|
||||
<SyntaxHighlighter data={m} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
))
|
||||
: null}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { NetworkParametersQuery } from '@vegaprotocol/graphql';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
|
||||
export const NETWORK_PARAMETERS_QUERY = gql`
|
||||
query NetworkParametersQuery {
|
||||
@ -14,8 +16,10 @@ const NetworkParameters = () => {
|
||||
const { data } = useQuery<NetworkParametersQuery>(NETWORK_PARAMETERS_QUERY);
|
||||
return (
|
||||
<section>
|
||||
<h1 data-testid="network-param-header">NetworkParameters</h1>
|
||||
<pre data-testid="parameters">{JSON.stringify(data, null, ' ')}</pre>
|
||||
<RouteTitle data-testid="network-param-header">
|
||||
Network Parameters
|
||||
</RouteTitle>
|
||||
{data ? <SyntaxHighlighter data={data} /> : null}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,57 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../router-config';
|
||||
|
||||
export const JumpToParty = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
() => (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const target = e.target as typeof e.target & {
|
||||
partyId: { value: number };
|
||||
};
|
||||
|
||||
const partyId = target.partyId.value;
|
||||
|
||||
if (partyId) {
|
||||
navigate(`/${Routes.PARTIES}/${partyId}`);
|
||||
}
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label
|
||||
htmlFor="block-input"
|
||||
className="block uppercase text-h5 font-bold"
|
||||
>
|
||||
Go to party
|
||||
</label>
|
||||
<input
|
||||
id="block-input"
|
||||
name="partyId"
|
||||
placeholder="Party id"
|
||||
className="bg-white-25 border-white border px-8 py-4 placeholder-white-60"
|
||||
/>
|
||||
<input
|
||||
className="border-white border px-28 py-4 cursor-pointer"
|
||||
type="submit"
|
||||
value="Go"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const Parties = () => {
|
||||
return (
|
||||
<section>
|
||||
<h1>Parties</h1>
|
||||
<h2>
|
||||
Not sure what to do with this page? Could show all parties but would
|
||||
eventually need to be rewritten. But that's not very useful either
|
||||
</h2>
|
||||
<RouteTitle data-testid="parties-header">Parties</RouteTitle>
|
||||
<JumpToParty />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,9 @@ import { useQuery } from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { SubHeading } from '../../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '../../../components/syntax-highlighter';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
|
||||
@ -47,10 +50,13 @@ const PARTY_ASSETS_QUERY = gql`
|
||||
|
||||
const Party = () => {
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
const {
|
||||
state: { data: partyData },
|
||||
} = useFetch<TendermintSearchTransactionResponse>(
|
||||
`${DATA_SOURCES.tendermintWebsocketUrl}/tx_search?query="tx.submitter=%27${party}%27"`
|
||||
`${
|
||||
DATA_SOURCES.tendermintUrl
|
||||
}/tx_search?query="tx.submitter='${party?.replace('0x', '')}'"`
|
||||
);
|
||||
|
||||
const { data } = useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(
|
||||
@ -65,11 +71,20 @@ const Party = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Party</h1>
|
||||
<h2>Tendermint Data</h2>
|
||||
<pre>{JSON.stringify(partyData, null, ' ')}</pre>
|
||||
<h2>Asset data</h2>
|
||||
<pre>{JSON.stringify(data, null, ' ')}</pre>
|
||||
<RouteTitle data-testid="parties-header">Party</RouteTitle>
|
||||
{data ? (
|
||||
<>
|
||||
<SubHeading>Asset data</SubHeading>
|
||||
<SyntaxHighlighter data={data} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{partyData ? (
|
||||
<>
|
||||
<SubHeading>Tendermint Data</SubHeading>
|
||||
<SyntaxHighlighter data={partyData} />
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import { TendermintUnconfirmedTransactionsResponse } from '../txs/tendermint-unconfirmed-transactions-response.d';
|
||||
import { TxList } from '../../components/txs';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
|
||||
const PendingTxs = () => {
|
||||
const {
|
||||
@ -13,7 +14,9 @@ const PendingTxs = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Unconfirmed transactions</h1>
|
||||
<RouteTitle data-testid="unconfirmed-transactions-header">
|
||||
Unconfirmed transactions
|
||||
</RouteTitle>
|
||||
https://lb.testnet.vega.xyz/tm/unconfirmed_txs
|
||||
<br />
|
||||
<div>Number: {unconfirmedTransactions?.result?.n_txs || 0}</div>
|
||||
|
@ -4,9 +4,10 @@ import useFetch from '../../../hooks/use-fetch';
|
||||
import { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
|
||||
import { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { TxContent, TxDetails } from '../../../components/txs';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { TxContent } from './tx-content';
|
||||
import { TxDetails } from './tx-details';
|
||||
|
||||
const Tx = () => {
|
||||
const { txHash } = useParams<{ txHash: string }>();
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { StatusMessage } from '../../../components/status-message';
|
||||
import { SyntaxHighlighter } from '../../../components/syntax-highlighter';
|
||||
import {
|
||||
Table,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../../../components/table';
|
||||
import { TxOrderType } from '../../../components/txs';
|
||||
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
|
||||
import { SyntaxHighlighter } from '../../syntax-highlighter';
|
||||
import { Table, TableRow, TableHeader, TableCell } from '../../table';
|
||||
import { TxOrderType } from '../tx-order-type';
|
||||
import { StatusMessage } from '../../status-message';
|
||||
|
||||
interface TxContentProps {
|
||||
data: ChainExplorerTxResponse | undefined;
|
@ -1,8 +1,13 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Table,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../../../components/table';
|
||||
import { TruncateInline } from '../../../components/truncate/truncate';
|
||||
import { Routes } from '../../../routes/router-config';
|
||||
import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
|
||||
import { Table, TableRow, TableCell, TableHeader } from '../../table';
|
||||
import { TruncateInline } from '../../truncate/truncate';
|
||||
|
||||
interface TxDetailsProps {
|
||||
txData: Result | undefined;
|
@ -1,5 +1,8 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '../../components/syntax-highlighter';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import { TendermintValidatorsResponse } from './tendermint-validator-response';
|
||||
@ -41,13 +44,21 @@ const Validators = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Validators</h1>
|
||||
<h2 data-testid="tendermint-header">Tendermint data</h2>
|
||||
<pre data-testid="tendermint-data">
|
||||
{JSON.stringify(validators, null, ' ')}
|
||||
</pre>
|
||||
<h2 data-testid="vega-header">Vega data</h2>
|
||||
<pre data-testid="vega-data">{JSON.stringify(data, null, ' ')}</pre>
|
||||
<RouteTitle data-testid="validators-header">Validators</RouteTitle>
|
||||
{data ? (
|
||||
<>
|
||||
<SubHeading data-testid="vega-header">Vega data</SubHeading>
|
||||
<SyntaxHighlighter data-testid="vega-data" data={data} />
|
||||
</>
|
||||
) : null}
|
||||
{validators ? (
|
||||
<>
|
||||
<SubHeading data-testid="tendermint-header">
|
||||
Tendermint data
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data-testid="tendermint-data" data={validators} />
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +0,0 @@
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
@ -9,6 +9,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" class="dark"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
|
@ -6,4 +6,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/ui-toolkit',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
@ -8,6 +8,11 @@ import { Intent } from '../../utils/intent';
|
||||
export default {
|
||||
title: 'Callout',
|
||||
component: Callout,
|
||||
argTypes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Callout>;
|
||||
|
||||
const Template: ComponentStory<typeof Callout> = (args) => (
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Callout } from '.';
|
||||
import { Intent } from '../../utils/intent';
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { EtherscanLink } from '.';
|
||||
import { EthereumChainIds } from '../../utils/web3';
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { FormGroup } from './form-group';
|
||||
|
||||
describe('FormGroup', () => {
|
||||
it('should render label if given a label', () => {
|
||||
render(
|
||||
<FormGroup label="label" labelFor="test">
|
||||
<input id="test"></input>
|
||||
</FormGroup>
|
||||
);
|
||||
expect(screen.getByLabelText('label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add classes passed in', () => {
|
||||
render(
|
||||
<FormGroup label="label" labelFor="test" className="fighter">
|
||||
<input id="test"></input>
|
||||
</FormGroup>
|
||||
);
|
||||
expect(screen.getByTestId('form-group')).toHaveClass('fighter');
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
render(
|
||||
<FormGroup label="label" labelFor="test" className="fighter">
|
||||
<input data-testid="foo" id="test"></input>
|
||||
</FormGroup>
|
||||
);
|
||||
expect(screen.getByTestId('foo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
32
libs/ui-toolkit/src/components/form-group/form-group.tsx
Normal file
32
libs/ui-toolkit/src/components/form-group/form-group.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface FormGroupProps {
|
||||
children: ReactNode;
|
||||
label?: string;
|
||||
labelFor?: string;
|
||||
labelAlign?: 'left' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FormGroup = ({
|
||||
children,
|
||||
label,
|
||||
labelFor,
|
||||
labelAlign = 'left',
|
||||
className,
|
||||
}: FormGroupProps) => {
|
||||
const labelClasses = classNames('block text-ui mb-4', {
|
||||
'text-right': labelAlign === 'right',
|
||||
});
|
||||
return (
|
||||
<div data-testid="form-group" className={classNames(className, 'mb-20')}>
|
||||
{label && (
|
||||
<label className={labelClasses} htmlFor={labelFor}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { Input } from '../input';
|
||||
import { FormGroup } from './form-group';
|
||||
export default {
|
||||
component: FormGroup,
|
||||
title: 'FormGroup',
|
||||
argTypes: {
|
||||
label: {
|
||||
type: 'string',
|
||||
},
|
||||
labelFor: {
|
||||
type: 'string',
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<FormGroup {...args} label="label" labelFor="test">
|
||||
<Input id="labelFor" />
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
label: 'label',
|
||||
labelFor: 'labelFor',
|
||||
};
|
@ -1,30 +1 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface FormGroupProps {
|
||||
children: ReactNode;
|
||||
label?: string;
|
||||
labelFor?: string;
|
||||
labelAlign?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const FormGroup = ({
|
||||
children,
|
||||
label,
|
||||
labelFor,
|
||||
labelAlign = 'left',
|
||||
}: FormGroupProps) => {
|
||||
const labelClasses = classNames('block text-ui mb-4', {
|
||||
'text-right': labelAlign === 'right',
|
||||
});
|
||||
return (
|
||||
<div className="mb-20">
|
||||
{label && (
|
||||
<label className={labelClasses} htmlFor={labelFor}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export * from './form-group';
|
||||
|
@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
interface InputErrorProps {
|
||||
interface InputErrorProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
intent?: 'danger' | 'warning';
|
||||
@ -11,6 +12,7 @@ export const InputError = ({
|
||||
intent = 'danger',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: InputErrorProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
[
|
||||
@ -33,7 +35,7 @@ export const InputError = ({
|
||||
'fill-intent-warning': intent === 'warning',
|
||||
});
|
||||
return (
|
||||
<div className={effectiveClassName}>
|
||||
<div className={effectiveClassName} {...props}>
|
||||
<Icon name="warning-sign" className={iconClassName} />
|
||||
{children}
|
||||
</div>
|
||||
|
5
libs/ui-toolkit/src/setup-tests.ts
Normal file
5
libs/ui-toolkit/src/setup-tests.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
Loading…
Reference in New Issue
Block a user