Add setting custom cluster endpoint

This commit is contained in:
Nathaniel Parke 2020-10-09 15:00:38 +08:00
parent d332181ae8
commit 2b5e310cdc
6 changed files with 271 additions and 143 deletions

View File

@ -0,0 +1,62 @@
import React, {useState} from "react";
import {Col, Input, Modal, Row} from "antd";
import {EndpointInfo} from "../utils/types";
export default function CustomClusterEndpointDialog({
visible,
onAddCustomEndpoint,
onClose,
} : {
visible: boolean;
onAddCustomEndpoint: (info: EndpointInfo) => void;
onClose?: () => void;
}) {
const [ customEndpoint, setCustomEndpoint] = useState('');
const [ customEndpointName, setCustomEndpointName] = useState('');
const onSubmit = () => {
const params = {
name: customEndpointName,
endpoint: customEndpoint,
custom: true,
}
onAddCustomEndpoint(params);
onDoClose();
};
const onDoClose = () => {
setCustomEndpoint('')
setCustomEndpointName('')
onClose && onClose();
};
const canSubmit = customEndpoint !== '' && customEndpointName !== '';
return (
<Modal
title={'Add custom endpoint'}
visible={visible}
onOk={onSubmit}
okText={'Add'}
onCancel={onDoClose}
okButtonProps={{ disabled: !canSubmit }}
>
<Row style={{ marginBottom: 8 }}>
<Col span={24}>
<Input
placeholder="Cluster Name"
value={customEndpointName}
onChange={(e) => setCustomEndpointName(e.target.value)}
/>
</Col>
</Row>
<Row style={{ marginBottom: 8 }}>
<Col span={24}>
<Input
placeholder="Cluster Endpoint"
value={customEndpoint}
onChange={(e) => setCustomEndpoint(e.target.value)}
/>
</Col>
</Row>
</Modal>
);
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { LinkOutlined } from '@ant-design/icons'; import { LinkOutlined } from '@ant-design/icons';
export default function LinkAddress({ title, address }) { export default function LinkAddress({ title, address }: {title?: undefined | any; address: string;}) {
return ( return (
<div> <div>
{title && <p style={{ color: 'white' }}>{title}</p>} {title && <p style={{ color: 'white' }}>{title}</p>}

View File

@ -1,131 +0,0 @@
import {
InfoCircleOutlined,
SettingOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Button, Menu, Popover, Select } from 'antd';
import React, { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import logo from '../assets/logo.svg';
import styled from 'styled-components';
import { useWallet, WALLET_PROVIDERS } from '../utils/wallet';
import { ENDPOINTS, useConnectionConfig } from '../utils/connection';
import LinkAddress from './LinkAddress';
import Settings from './Settings';
const Wrapper = styled.div`
background-color: #0d1017;
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0px 30px;
flex-wrap: wrap;
`;
const LogoWrapper = styled.div`
display: flex;
align-items: center;
color: #2abdd2;
font-weight: bold;
cursor: pointer;
img {
height: 30px;
margin-right: 8px;
}
`;
export default function TopBar() {
const { connected, wallet, providerUrl, setProvider } = useWallet();
const { endpoint, setEndpoint } = useConnectionConfig();
const location = useLocation();
const history = useHistory();
const publicKey = wallet?.publicKey?.toBase58();
const handleClick = useCallback(
(e) => {
history.push(e.key);
},
[history],
);
return (
<Wrapper>
<LogoWrapper>
<img src={logo} alt="" onClick={() => history.push('/')} />
{'SERUM'}
</LogoWrapper>
<Menu
mode="horizontal"
onClick={handleClick}
selectedKeys={[location.pathname]}
style={{
borderBottom: 'none',
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'flex-end',
flex: 1,
}}
>
<Menu.Item key="/">TRADE</Menu.Item>
</Menu>
{connected && (
<div>
<Popover
content={<Settings autoApprove={wallet?.autoApprove} />}
placement="bottomRight"
title="Settings"
trigger="click"
>
<Button style={{ marginRight: 8 }}>
<SettingOutlined />
Settings
</Button>
</Popover>
</div>
)}
<div>
<Select
onSelect={setEndpoint}
value={endpoint}
style={{ marginRight: 8 }}
>
{ENDPOINTS.map(({ name, endpoint }) => (
<Select.Option value={endpoint} key={endpoint}>
{name}
</Select.Option>
))}
</Select>
</div>
<div>
<Select onSelect={setProvider} value={providerUrl}>
{WALLET_PROVIDERS.map(({ name, url }) => (
<Select.Option value={url} key={url}>
{name}
</Select.Option>
))}
</Select>
</div>
<div>
<Button
type="text"
size="large"
onClick={connected ? wallet.disconnect : wallet.connect}
style={{ color: '#2abdd2' }}
>
<UserOutlined />
{!connected ? 'Connect wallet' : 'Disconnect'}
</Button>
{connected && (
<Popover
content={<LinkAddress address={publicKey} />}
placement="bottomRight"
title="Wallet public key"
trigger="click"
>
<InfoCircleOutlined style={{ color: '#2abdd2' }} />
</Popover>
)}
</div>
</Wrapper>
);
}

178
src/components/TopBar.tsx Normal file
View File

@ -0,0 +1,178 @@
import {InfoCircleOutlined, PlusCircleOutlined, SettingOutlined, UserOutlined,} from '@ant-design/icons';
import {Button, Col, Menu, Popover, Row, Select} from 'antd';
import React, {useCallback, useState} from 'react';
import {useHistory, useLocation} from 'react-router-dom';
import logo from '../assets/logo.svg';
import styled from 'styled-components';
import {useWallet, WALLET_PROVIDERS} from '../utils/wallet';
import {useConnectionConfig} from '../utils/connection';
import LinkAddress from './LinkAddress';
import Settings from './Settings';
import CustomClusterEndpointDialog from "./CustomClusterEndpointDialog";
import {EndpointInfo} from "../utils/types";
import {notify} from "../utils/notifications";
const Wrapper = styled.div`
background-color: #0d1017;
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0px 30px;
flex-wrap: wrap;
`;
const LogoWrapper = styled.div`
display: flex;
align-items: center;
color: #2abdd2;
font-weight: bold;
cursor: pointer;
img {
height: 30px;
margin-right: 8px;
}
`;
export default function TopBar() {
const { connected, wallet, providerUrl, setProvider } = useWallet();
const { endpoint, setEndpoint, availableEndpoints, setCustomEndpoints } = useConnectionConfig();
const [ addEndpointVisible, setAddEndpointVisible ] = useState(false)
const location = useLocation();
const history = useHistory();
const publicKey = wallet?.publicKey?.toBase58();
const handleClick = useCallback(
(e) => {
history.push(e.key);
},
[history],
);
const onAddCustomEndpoint = (info: EndpointInfo) => {
const existingEndpoint = availableEndpoints.some(
(e) => e.endpoint === info.endpoint,
);
if (existingEndpoint) {
notify({
message: `An endpoint with the given url already exists`,
type: 'error',
});
return;
}
const newCustomEndpoints = [...availableEndpoints.filter(e => e.custom), info];
setEndpoint(info.name);
setCustomEndpoints(newCustomEndpoints);
}
return (
<>
<CustomClusterEndpointDialog
visible={addEndpointVisible}
onAddCustomEndpoint={onAddCustomEndpoint}
onClose={() => setAddEndpointVisible(false)}
/>
<Wrapper>
<LogoWrapper>
<img src={logo} alt="" onClick={() => history.push('/')} />
{'SERUM'}
</LogoWrapper>
<Menu
mode="horizontal"
onClick={handleClick}
selectedKeys={[location.pathname]}
style={{
borderBottom: 'none',
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'flex-end',
flex: 1,
}}
>
<Menu.Item key="/">TRADE</Menu.Item>
</Menu>
<div>
<Row
align="middle"
style={{ paddingLeft: 5, paddingRight: 5 }}
gutter={16}
>
<Col>
<PlusCircleOutlined
style={{ color: '#2abdd2' }}
onClick={() => setAddEndpointVisible(true)}
/>
</Col>
<Col>
<Popover
content={endpoint}
placement="bottomRight"
title="URL"
trigger="hover"
>
<InfoCircleOutlined style={{ color: '#2abdd2' }} />
</Popover>
</Col>
<Col>
<Select
onSelect={setEndpoint}
value={endpoint}
style={{ marginRight: 8, width: '150px' }}
>
{availableEndpoints.map(({ name, endpoint }) => (
<Select.Option value={endpoint} key={endpoint}>
{name}
</Select.Option>
))}
</Select>
</Col>
</Row>
</div>
{connected && (
<div>
<Popover
content={<Settings autoApprove={wallet?.autoApprove} />}
placement="bottomRight"
title="Settings"
trigger="click"
>
<Button style={{ marginRight: 8 }}>
<SettingOutlined />
Settings
</Button>
</Popover>
</div>
)}
<div>
<Select onSelect={setProvider} value={providerUrl}>
{WALLET_PROVIDERS.map(({ name, url }) => (
<Select.Option value={url} key={url}>
{name}
</Select.Option>
))}
</Select>
</div>
<div>
<Button
type="text"
size="large"
onClick={connected ? wallet.disconnect : wallet.connect}
style={{ color: '#2abdd2' }}
>
<UserOutlined />
{!connected ? 'Connect wallet' : 'Disconnect'}
</Button>
{connected && (
<Popover
content={<LinkAddress address={publicKey} />}
placement="bottomRight"
title="Wallet public key"
trigger="click"
>
<InfoCircleOutlined style={{ color: '#2abdd2' }} />
</Popover>
)}
</div>
</Wrapper>
</>
);
}

View File

@ -1,18 +1,19 @@
import { useLocalStorageState } from './utils'; import {useLocalStorageState} from './utils';
import { Account, AccountInfo, clusterApiUrl, Connection, PublicKey } from '@solana/web3.js'; import {Account, AccountInfo, clusterApiUrl, Connection, PublicKey} from '@solana/web3.js';
import React, { useContext, useEffect, useMemo } from 'react'; import React, {useContext, useEffect, useMemo} from 'react';
import { setCache, useAsyncData } from './fetch-loop'; import {setCache, useAsyncData} from './fetch-loop';
import tuple from 'immutable-tuple'; import tuple from 'immutable-tuple';
import {ConnectionContextValues} from "./types"; import {ConnectionContextValues, EndpointInfo} from "./types";
export const ENDPOINTS: {name: string; endpoint: string;}[] = [ export const ENDPOINTS: EndpointInfo[] = [
{ {
name: 'mainnet-beta', name: 'mainnet-beta',
endpoint: 'https://solana-api.projectserum.com', endpoint: 'https://solana-api.projectserum.com',
custom: false
}, },
{ name: 'testnet', endpoint: clusterApiUrl('testnet') }, { name: 'testnet', endpoint: clusterApiUrl('testnet'), custom: false },
{ name: 'devnet', endpoint: clusterApiUrl('devnet') }, { name: 'devnet', endpoint: clusterApiUrl('devnet'), custom: false },
{ name: 'localnet', endpoint: 'http://127.0.0.1:8899' }, { name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false },
]; ];
const accountListenerCount = new Map(); const accountListenerCount = new Map();
@ -24,6 +25,11 @@ export function ConnectionProvider({ children }) {
'connectionEndpts', 'connectionEndpts',
ENDPOINTS[0].endpoint, ENDPOINTS[0].endpoint,
); );
const [customEndpoints, setCustomEndpoints] = useLocalStorageState<EndpointInfo[]>(
'customConnectionEndpoints',
[]
)
const availableEndpoints = ENDPOINTS.concat(customEndpoints);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [ const connection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint, endpoint,
@ -60,7 +66,7 @@ export function ConnectionProvider({ children }) {
return ( return (
<ConnectionContext.Provider <ConnectionContext.Provider
value={{ endpoint, setEndpoint, connection, sendConnection }} value={{ endpoint, setEndpoint, connection, sendConnection, availableEndpoints, setCustomEndpoints }}
> >
{children} {children}
</ConnectionContext.Provider> </ConnectionContext.Provider>
@ -88,7 +94,12 @@ export function useConnectionConfig() {
if (!context) { if (!context) {
throw new Error('Missing connection context') throw new Error('Missing connection context')
} }
return { endpoint: context.endpoint, setEndpoint: context.setEndpoint }; return {
endpoint: context.endpoint,
setEndpoint: context.setEndpoint,
availableEndpoints: context.availableEndpoints,
setCustomEndpoints: context.setCustomEndpoints,
};
} }
export function useAccountInfo(publicKey: PublicKey | undefined | null): [AccountInfo<Buffer> | null | undefined, boolean] { export function useAccountInfo(publicKey: PublicKey | undefined | null): [AccountInfo<Buffer> | null | undefined, boolean] {

View File

@ -9,6 +9,8 @@ export interface ConnectionContextValues {
setEndpoint: (newEndpoint: string) => void; setEndpoint: (newEndpoint: string) => void;
connection: Connection; connection: Connection;
sendConnection: Connection; sendConnection: Connection;
availableEndpoints: EndpointInfo[];
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void;
} }
export interface WalletContextValues { export interface WalletContextValues {
@ -106,3 +108,9 @@ export interface PreferencesContextValues {
autoSettleEnabled: boolean; autoSettleEnabled: boolean;
setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void; setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void;
} }
export interface EndpointInfo {
name: string;
endpoint: string;
custom: boolean;
}