Feat/83 switching vega key (#156)

* add manage dialog to wallet lib, add it to trading app

* add test for wallet button

* add tests for manage dialog

* move tooltip to ui-toolkit, add copy with tooltip component for manage dialog

* add better labelling

* add tooltip story

* add story for copy-with-tooltip

* add tests for tooltip and copy-with-tooltip

* move useFakeTimers call to beforeAll

* adjust design of manage dialog

* fix linting issues
This commit is contained in:
Matthew Russell 2022-03-31 10:16:30 -07:00 committed by GitHub
parent e5b2c360ce
commit 15551b65e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 572 additions and 119 deletions

View File

@ -1,7 +1,6 @@
import { truncateByChars } from '@vegaprotocol/react-helpers';
import * as React from 'react';
const ELLIPSIS = '\u2026';
interface TruncateInlineProps {
text: string | null;
className?: string;
@ -41,16 +40,3 @@ export function TruncateInline({
return <span {...wrapperProps}>{truncatedText}</span>;
}
}
export function truncateByChars(s: string, startChars = 6, endChars = 6) {
// if the text is shorted than the total number of chars to show
// no truncation is needed. Plus one is to account for the ellipsis
if (s.length <= startChars + endChars + 1) {
return s;
}
const start = s.slice(0, startChars);
const end = s.slice(-endChars);
return start + ELLIPSIS + end;
}

View File

@ -1,30 +1 @@
import { t } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface VegaWalletButtonProps {
setConnectDialog: (isOpen: boolean) => void;
}
export const VegaWalletButton = ({
setConnectDialog,
}: VegaWalletButtonProps) => {
const { disconnect, keypairs } = useVegaWallet();
const isConnected = keypairs !== null;
const handleClick = () => {
if (isConnected) {
disconnect();
} else {
setConnectDialog(true);
}
};
return (
<button
onClick={handleClick}
className="ml-auto inline-block text-ui sm:text-body-large"
>
{isConnected ? t('Disconnect Vega wallet') : t('Connect Vega wallet')}
</button>
);
};
export { VegaWalletConnectButton } from './vega-wallet-connect-button';

View File

@ -0,0 +1,51 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import type { VegaWalletConnectButtonProps } from './vega-wallet-connect-button';
import { VegaWalletConnectButton } from './vega-wallet-connect-button';
let props: VegaWalletConnectButtonProps;
beforeEach(() => {
props = {
setConnectDialog: jest.fn(),
setManageDialog: jest.fn(),
};
});
const generateJsx = (
context: VegaWalletContextShape,
props: VegaWalletConnectButtonProps
) => {
return (
<VegaWalletContext.Provider value={context}>
<VegaWalletConnectButton {...props} />
</VegaWalletContext.Provider>
);
};
test('Not connected', () => {
render(generateJsx({ keypair: null } as VegaWalletContextShape, props));
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Connect Vega wallet');
fireEvent.click(button);
expect(props.setConnectDialog).toHaveBeenCalledWith(true);
expect(props.setManageDialog).not.toHaveBeenCalled();
});
test('Connected', () => {
render(
generateJsx(
{ keypair: { pub: '123456__123456' } } as VegaWalletContextShape,
props
)
);
expect(screen.getByText('Vega key:')).toBeInTheDocument();
const button = screen.getByRole('button');
expect(button).toHaveTextContent('123456\u2026123456');
fireEvent.click(button);
expect(props.setManageDialog).toHaveBeenCalledWith(true);
expect(props.setConnectDialog).not.toHaveBeenCalled();
});

View File

@ -0,0 +1,37 @@
import { truncateByChars } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
export interface VegaWalletConnectButtonProps {
setConnectDialog: (isOpen: boolean) => void;
setManageDialog: (isOpen: boolean) => void;
}
export const VegaWalletConnectButton = ({
setConnectDialog,
setManageDialog,
}: VegaWalletConnectButtonProps) => {
const { keypair } = useVegaWallet();
const isConnected = keypair !== null;
const handleClick = () => {
if (isConnected) {
setManageDialog(true);
} else {
setConnectDialog(true);
}
};
return (
<span>
{isConnected && (
<span className="text-ui-small font-mono mr-2">Vega key:</span>
)}
<button
onClick={handleClick}
className="ml-auto inline-block text-ui-small font-mono hover:underline"
>
{isConnected ? truncateByChars(keypair.pub) : 'Connect Vega wallet'}
</button>
</span>
);
};

View File

@ -1,31 +1,29 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { Navbar } from '../components/navbar';
import { t, ThemeContext } from '@vegaprotocol/react-helpers';
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
VegaConnectDialog,
VegaManageDialog,
VegaWalletProvider,
} from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { createClient } from '../lib/apollo-client';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { ApolloProvider } from '@apollo/client';
import { AppLoader } from '../components/app-loader';
import { VegaWalletButton } from '../components/vega-wallet-connect-button';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
import './styles.css';
function VegaTradingApp({ Component, pageProps }: AppProps) {
const client = useMemo(() => createClient(process.env['NX_VEGA_URL']), []);
const [dialogOpen, setDialogOpen] = useState(false);
const [vegaWallet, setVegaWallet] = useState({
connect: false,
manage: false,
});
const [theme, toggleTheme] = useThemeSwitcher();
const setConnectDialog = useCallback((isOpen?: boolean) => {
setDialogOpen((curr) => {
if (isOpen === undefined) return !curr;
return isOpen;
});
}, []);
return (
<ThemeContext.Provider value={theme}>
<ApolloProvider client={client}>
@ -42,8 +40,15 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
<div className="h-full dark:bg-black dark:text-white-60 bg-white text-black-60 grid grid-rows-[min-content,1fr]">
<div className="flex items-stretch border-b-[7px] border-vega-yellow">
<Navbar />
<div className="flex items-center ml-auto mr-8">
<VegaWalletButton setConnectDialog={setConnectDialog} />
<div className="flex items-center gap-4 ml-auto mr-8">
<VegaWalletConnectButton
setConnectDialog={(open) =>
setVegaWallet((x) => ({ ...x, connect: open }))
}
setManageDialog={(open) =>
setVegaWallet((x) => ({ ...x, manage: open }))
}
/>
<ThemeSwitcher onToggle={toggleTheme} className="-my-4" />
</div>
</div>
@ -52,8 +57,16 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
</main>
<VegaConnectDialog
connectors={Connectors}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
dialogOpen={vegaWallet.connect}
setDialogOpen={(open) =>
setVegaWallet((x) => ({ ...x, connect: open }))
}
/>
<VegaManageDialog
dialogOpen={vegaWallet.manage}
setDialogOpen={(open) =>
setVegaWallet((x) => ({ ...x, manage: open }))
}
/>
</div>
</AppLoader>

View File

@ -1,4 +1,4 @@
import { Tooltip } from '../tooltip';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import type { StatFields } from '../../config/types';
import { defaultFieldFormatter } from '../table-row';
import { GoodThresholdIndicator } from '../good-threshold-indicator';
@ -11,7 +11,7 @@ export const PromotedStatsItem = ({
description,
}: StatFields) => {
return (
<Tooltip description={description}>
<Tooltip description={description} align="start">
<div className="px-24 py-16 pr-64 border items-center">
<div className="uppercase text-[0.9375rem]">
<GoodThresholdIndicator goodThreshold={goodThreshold} value={value} />

View File

@ -1,4 +1,4 @@
import { Tooltip } from '../tooltip';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import type { StatFields } from '../../config/types';
import { GoodThresholdIndicator } from '../good-threshold-indicator';
@ -13,7 +13,7 @@ export const TableRow = ({
description,
}: StatFields) => {
return (
<Tooltip description={description}>
<Tooltip description={description} align="start">
<tr className="border">
<td className="py-4 px-8">{title}</td>
<td className="py-4 px-8 text-right">

View File

@ -1,6 +1,4 @@
export * from './lib/context';
export * from './lib/datetime';
export * from './lib/decimals';
export * from './lib/format';
export * from './lib/grid-cells';
export * from './lib/storage';

View File

@ -1,13 +0,0 @@
/** Returns date in a format suitable for input[type=date] elements */
export const formatForInput = (date: Date) => {
const padZero = (num: number) => num.toString().padStart(2, '0');
const year = date.getFullYear();
const month = padZero(date.getMonth() + 1);
const day = padZero(date.getDate());
const hours = padZero(date.getHours());
const minutes = padZero(date.getMinutes());
const secs = padZero(date.getSeconds());
return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
};

View File

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

View File

@ -1,12 +0,0 @@
import { BigNumber } from 'bignumber.js';
export function addDecimal(value: string, decimals: number): string {
if (!decimals) return value;
return new BigNumber(value || 0)
.dividedBy(Math.pow(10, decimals))
.toFixed(decimals);
}
export function removeDecimal(value: string, decimals: number): string {
if (!decimals) return value;
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
}

View File

@ -1,11 +1,5 @@
import once from 'lodash/once';
import memoize from 'lodash/memoize';
import { addDecimal } from '../decimals';
const getUserLocale = () => 'default';
export const splitAt = (index: number) => (x: string) =>
[x.slice(0, index), x.slice(index)];
import { getUserLocale } from './utils';
/**
* Returns a number prefixed with either a '-' or a '+'. The open volume field
@ -50,18 +44,20 @@ export const getDateTimeFormat = once(
})
);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getNumberFormat = memoize(
(minimumFractionDigits: number) =>
new Intl.NumberFormat(getUserLocale(), { minimumFractionDigits })
);
export const getRelativeTimeFormat = once(
() => new Intl.RelativeTimeFormat(getUserLocale())
);
export const formatNumber = (rawValue: string, decimalPlaces: number) => {
const x = addDecimal(rawValue, decimalPlaces);
/** Returns date in a format suitable for input[type=date] elements */
export const formatForInput = (date: Date) => {
const padZero = (num: number) => num.toString().padStart(2, '0');
return getNumberFormat(decimalPlaces).format(Number(x));
const year = date.getFullYear();
const month = padZero(date.getMonth() + 1);
const day = padZero(date.getDate());
const hours = padZero(date.getHours());
const minutes = padZero(date.getMinutes());
const secs = padZero(date.getSeconds());
return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
};

View File

@ -1 +1,4 @@
export * from './format';
export * from './date';
export * from './number';
export * from './truncate';
export * from './utils';

View File

@ -0,0 +1,27 @@
import { BigNumber } from 'bignumber.js';
import memoize from 'lodash/memoize';
import { getUserLocale } from './utils';
export function addDecimal(value: string, decimals: number): string {
if (!decimals) return value;
return new BigNumber(value || 0)
.dividedBy(Math.pow(10, decimals))
.toFixed(decimals);
}
export function removeDecimal(value: string, decimals: number): string {
if (!decimals) return value;
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getNumberFormat = memoize(
(minimumFractionDigits: number) =>
new Intl.NumberFormat(getUserLocale(), { minimumFractionDigits })
);
export const formatNumber = (rawValue: string, decimalPlaces: number) => {
const x = addDecimal(rawValue, decimalPlaces);
return getNumberFormat(decimalPlaces).format(Number(x));
};

View File

@ -0,0 +1,14 @@
export function truncateByChars(s: string, startChars = 6, endChars = 6) {
const ELLIPSIS = '\u2026';
// if the text is shorted than the total number of chars to show
// no truncation is needed. Plus one is to account for the ellipsis
if (s.length <= startChars + endChars + 1) {
return s;
}
const start = s.slice(0, startChars);
const end = s.slice(-endChars);
return start + ELLIPSIS + end;
}

View File

@ -0,0 +1,4 @@
export const getUserLocale = () => 'default';
export const splitAt = (index: number) => (x: string) =>
[x.slice(0, index), x.slice(index)];

View File

@ -151,7 +151,7 @@ module.exports = {
body: ['14px', '20px'],
ui: ['14px', '20px'],
'ui-small': ['10px', '16px'],
'ui-small': ['12px', '16px'],
},
boxShadow: {

View File

@ -0,0 +1,29 @@
import {
fireEvent,
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import { CopyWithTooltip } from './copy-with-tooltip';
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('CopyWithTooltip', async () => {
const copyText = 'Text to be copied';
render(
<CopyWithTooltip text={copyText}>
<button>Copy</button>
</CopyWithTooltip>
);
fireEvent.click(screen.getByText('Copy'));
expect(screen.getByRole('tooltip')).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByRole('tooltip'));
});

View File

@ -0,0 +1,24 @@
import type { Story, Meta } from '@storybook/react';
import type { CopyWithTooltipProps } from './copy-with-tooltip';
import { CopyWithTooltip } from './copy-with-tooltip';
export default {
component: CopyWithTooltip,
title: 'CopyWithTooltip',
} as Meta;
const Template: Story<CopyWithTooltipProps> = (args) => (
<div>
<p>
<span>{args.text}</span>
{' | '}
<CopyWithTooltip {...args} />
</p>
</div>
);
export const Default = Template.bind({});
Default.args = {
children: <button className="underline">Copy</button>,
text: 'Lorem ipsum dolor sit',
};

View File

@ -0,0 +1,41 @@
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
import { Tooltip } from '../tooltip';
import CopyToClipboard from 'react-copy-to-clipboard';
export const TOOLTIP_TIMEOUT = 800;
export interface CopyWithTooltipProps {
children: ReactElement;
text: string;
}
export function CopyWithTooltip({ children, text }: CopyWithTooltipProps) {
const [copied, setCopied] = useState(false);
useEffect(() => {
// eslint-disable-next-line
let timeout: any;
if (copied) {
timeout = setTimeout(() => {
setCopied(false);
}, TOOLTIP_TIMEOUT);
}
return () => {
clearTimeout(timeout);
};
}, [copied]);
return (
<CopyToClipboard text={text} onCopy={() => setCopied(true)}>
{/* Needs this wrapping div as tooltip component interfers with element used to capture click for copy */}
<span>
<Tooltip description="Copied" open={copied} align="center">
{children}
</Tooltip>
</span>
</CopyToClipboard>
);
}

View File

@ -0,0 +1 @@
export { CopyWithTooltip } from './copy-with-tooltip';

View File

@ -12,6 +12,8 @@ interface IconProps {
export const Icon = ({ size = 16, name, className }: IconProps) => {
const effectiveClassName = classNames(
'inline-block',
'fill-current',
{
'w-20': size === 20,
'h-20': size === 20,

View File

@ -0,0 +1,28 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { Tooltip } from './tooltip';
test('Renders a tooltip', async () => {
const props = {
description: 'description',
children: <button>Tooltip</button>,
};
render(<Tooltip {...props} />);
// radix applies the data-state attribute
expect(screen.getByRole('button')).toHaveAttribute('data-state', 'closed');
fireEvent.mouseOver(screen.getByRole('button'));
expect(await screen.findByRole('tooltip')).toBeInTheDocument();
});
test('Doesnt render a tooltip if no description provided', () => {
const props = {
description: undefined,
children: <button>Tooltip</button>,
};
render(<Tooltip {...props} />);
expect(screen.getByRole('button')).not.toHaveAttribute(
'data-state',
'closed'
);
fireEvent.mouseOver(screen.getByRole('button'));
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});

View File

@ -0,0 +1,23 @@
import type { Story, Meta } from '@storybook/react';
import type { TooltipProps } from './tooltip';
import { Tooltip } from './tooltip';
export default {
component: Tooltip,
title: 'Tooltip',
} as Meta;
const Template: Story<TooltipProps> = (args) => <Tooltip {...args} />;
export const Uncontrolled = Template.bind({});
Uncontrolled.args = {
children: <button>Click me!</button>,
description: 'Tooltip content!',
};
export const Controlled = Template.bind({});
Controlled.args = {
children: <button>Open me using the 'open' prop</button>,
description: 'Tooltip content!',
open: false,
};

View File

@ -7,18 +7,20 @@ import {
Arrow,
} from '@radix-ui/react-tooltip';
interface TooltipProps {
export interface TooltipProps {
children: React.ReactElement;
description?: string;
open?: boolean;
align?: 'start' | 'center' | 'end';
}
// Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({ children, description }: TooltipProps) =>
export const Tooltip = ({ children, description, open, align }: TooltipProps) =>
description ? (
<Provider delayDuration={200} skipDelayDuration={100}>
<Root>
<Root open={open}>
<Trigger asChild>{children}</Trigger>
<Content align={'start'} alignOffset={5}>
<Content align={align} alignOffset={5}>
<Arrow
width={10}
height={5}

View File

@ -5,6 +5,7 @@ export { AgGridLazy, AgGridDynamic } from './components/ag-grid';
export { AsyncRenderer } from './components/async-renderer';
export { Button, AnchorButton } from './components/button';
export { Callout } from './components/callout';
export { CopyWithTooltip } from './components/copy-with-tooltip';
export { EthereumUtils };
export { EtherscanLink } from './components/etherscan-link';
export { FormGroup } from './components/form-group';
@ -19,6 +20,7 @@ export { TextArea } from './components/text-area';
export { ThemeSwitcher } from './components/theme-switcher';
export { Dialog } from './components/dialog/dialog';
export { VegaLogo } from './components/vega-logo';
export { Tooltip } from './components/tooltip';
// Utils
export * from './utils/intent';

View File

@ -3,3 +3,32 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
// copy-to-clipboard used byreact-copy-to-clipboard falls back to
// window.prompt if more modern methods of accessing the clipboard api fail
window.prompt = jest.fn();
// Required by radix-ui/react-tooltip
global.ResizeObserver = ResizeObserver;
// Required by radix-ui/react-tooltip
global.DOMRect = class DOMRect {
bottom = 0;
left = 0;
right = 0;
top = 0;
constructor(
public x = 0,
public y = 0,
public width = 0,
public height = 0
) {}
static fromRect(other?: DOMRectInit): DOMRect {
return new DOMRect(other?.x, other?.y, other?.width, other?.height);
}
toJSON() {
return JSON.stringify(this);
}
};

View File

@ -6,4 +6,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/wallet',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};

View File

@ -6,3 +6,4 @@ export * from './connectors';
export * from './storage-keys';
export * from './types';
export * from './use-vega-transaction';
export * from './manage-dialog';

View File

@ -0,0 +1,82 @@
import { fireEvent, render, screen, within } from '@testing-library/react';
import { VegaWalletContext } from './context';
import type { VegaWalletContextShape, VegaKeyExtended } from './context';
import type { VegaManageDialogProps } from './manage-dialog';
import { VegaManageDialog } from './manage-dialog';
let props: VegaManageDialogProps;
let context: Partial<VegaWalletContextShape>;
let keypair1: VegaKeyExtended;
let keypair2: VegaKeyExtended;
beforeEach(() => {
keypair1 = {
pub: '111111__111111',
name: 'keypair1-name',
} as VegaKeyExtended;
keypair2 = {
pub: '222222__222222',
name: 'keypair2-name',
} as VegaKeyExtended;
props = {
dialogOpen: true,
setDialogOpen: jest.fn(),
};
context = {
keypair: keypair1,
keypairs: [keypair1, keypair2],
selectPublicKey: jest.fn(),
disconnect: jest.fn(),
};
});
const generateJsx = (
context: VegaWalletContextShape,
props: VegaManageDialogProps
) => {
return (
<VegaWalletContext.Provider value={context}>
<VegaManageDialog {...props} />
</VegaWalletContext.Provider>
);
};
test('Shows list of available keys and can disconnect', () => {
render(generateJsx(context as VegaWalletContextShape, props));
const list = screen.getByTestId('keypair-list');
expect(list).toBeInTheDocument();
// eslint-disable-next-line
expect(list.children).toHaveLength(context.keypairs!.length);
// eslint-disable-next-line
context.keypairs!.forEach((kp, i) => {
const keyListItem = within(screen.getByTestId(`key-${kp.pub}`));
expect(
keyListItem.getByText(kp.name, { selector: 'h2' })
).toBeInTheDocument();
expect(keyListItem.getByText('Copy')).toBeInTheDocument();
// Active
// eslint-disable-next-line
if (kp.pub === context.keypair!.pub) {
expect(keyListItem.getByTestId('selected-key')).toBeInTheDocument();
expect(
keyListItem.queryByTestId('select-keypair-button')
).not.toBeInTheDocument();
}
// Inactive
else {
const selectButton = keyListItem.getByTestId('select-keypair-button');
expect(selectButton).toBeInTheDocument();
expect(keyListItem.queryByTestId('selected-key')).not.toBeInTheDocument();
fireEvent.click(selectButton);
expect(context.selectPublicKey).toHaveBeenCalledWith(kp.pub);
}
});
// Disconnect
fireEvent.click(screen.getByTestId('disconnect'));
expect(context.disconnect).toHaveBeenCalled();
expect(props.setDialogOpen).toHaveBeenCalledWith(false);
});

View File

@ -0,0 +1,90 @@
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
import {
Button,
Dialog,
CopyWithTooltip,
Intent,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '.';
export interface VegaManageDialogProps {
dialogOpen: boolean;
setDialogOpen: (isOpen: boolean) => void;
}
export const VegaManageDialog = ({
dialogOpen,
setDialogOpen,
}: VegaManageDialogProps) => {
const { keypair, keypairs, selectPublicKey, disconnect } = useVegaWallet();
return (
<Dialog
title={t('SELECT A VEGA KEY')}
open={dialogOpen}
onChange={setDialogOpen}
intent={Intent.Prompt}
>
<div className="text-ui">
{keypairs ? (
<ul className="mb-12" data-testid="keypair-list">
{keypairs.map((kp) => {
return (
<li
key={kp.pub}
data-testid={`key-${kp.pub}`}
className="mb-24 last:mb-0"
>
<h2 className="mb-8 text-h5 capitalize">{kp.name}</h2>
{kp.pub === keypair?.pub ? (
<p
className="uppercase mb-8 font-bold"
data-testid="selected-key"
>
{t('Selected key')}
</p>
) : (
<Button
onClick={() => {
selectPublicKey(kp.pub);
setDialogOpen(false);
}}
disabled={kp.pub === keypair?.pub}
className="mb-8"
data-testid="select-keypair-button"
>
{t('Select this key')}
</Button>
)}
<div className="flex justify-between text-ui-small">
<p className="font-mono">
{truncateByChars(kp.pub, 23, 23)}
</p>
<CopyWithTooltip text={kp.pub}>
<button className="underline">
<Icon name="duplicate" className="mr-4" />
{t('Copy')}
</button>
</CopyWithTooltip>
</div>
</li>
);
})}
</ul>
) : null}
<div className="mt-24">
<Button
data-testid="disconnect"
variant="secondary"
onClick={() => {
disconnect();
setDialogOpen(false);
}}
>
{t('Disconnect all keys')}
</Button>
</div>
</div>
</Dialog>
);
};

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -46,6 +46,7 @@
"nx": "^13.8.3",
"postcss": "^8.4.6",
"react": "17.0.2",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "17.0.2",
"react-hook-form": "^7.27.0",
"react-syntax-highlighter": "^15.4.5",
@ -88,6 +89,7 @@
"@types/node": "16.11.7",
"@types/prismjs": "^1.26.0",
"@types/react": "17.0.30",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "17.0.9",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@typescript-eslint/eslint-plugin": "~5.10.0",
@ -111,6 +113,7 @@
"nx": "^13.8.3",
"prettier": "^2.5.1",
"react-test-renderer": "17.0.2",
"resize-observer-polyfill": "^1.5.1",
"sass": "1.43.2",
"storybook-addon-themes": "^6.1.0",
"ts-jest": "27.0.5",

View File

@ -5386,6 +5386,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-copy-to-clipboard@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#c29690b472a54edff35916f0d1c6c797ad0fd34b"
integrity sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==
dependencies:
"@types/react" "*"
"@types/react-dom@17.0.9":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
@ -8966,7 +8973,7 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-to-clipboard@^3.3.1:
copy-to-clipboard@^3, copy-to-clipboard@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
@ -16584,7 +16591,7 @@ prompts@^2.0.1, prompts@^2.4.0:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -16834,6 +16841,14 @@ react-colorful@^5.1.2:
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
react-copy-to-clipboard@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf"
integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
react-docgen-typescript@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
@ -17425,6 +17440,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"