diff --git a/apps/explorer/jest.config.js b/apps/explorer/jest.config.js
index 591654a09..fcdba1f33 100644
--- a/apps/explorer/jest.config.js
+++ b/apps/explorer/jest.config.js
@@ -7,4 +7,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/explorer',
+ setupFilesAfterEnv: ['./src/app/setupTests.ts'],
};
diff --git a/apps/explorer/src/app/components/render-fetched/index.tsx b/apps/explorer/src/app/components/render-fetched/index.tsx
index 1e03a2f11..233bc0996 100644
--- a/apps/explorer/src/app/components/render-fetched/index.tsx
+++ b/apps/explorer/src/app/components/render-fetched/index.tsx
@@ -19,7 +19,9 @@ export const RenderFetched = ({
}
if (error) {
- return Error: {error};
+ return (
+ Error retrieving data
+ );
}
return children;
diff --git a/apps/explorer/src/app/components/seconds-ago/index.tsx b/apps/explorer/src/app/components/seconds-ago/index.tsx
index 6d68ba550..965fd6024 100644
--- a/apps/explorer/src/app/components/seconds-ago/index.tsx
+++ b/apps/explorer/src/app/components/seconds-ago/index.tsx
@@ -4,7 +4,7 @@ interface SecondsAgoProps {
date: string | undefined;
}
-export const SecondsAgo = ({ date }: SecondsAgoProps) => {
+export const SecondsAgo = ({ date, ...props }: SecondsAgoProps) => {
const [now, setNow] = useState(Date.now());
useEffect(() => {
@@ -18,10 +18,11 @@ export const SecondsAgo = ({ date }: SecondsAgoProps) => {
return <>Date unknown>;
}
+
const timeAgoInSeconds = Math.floor((now - new Date(date).getTime()) / 1000);
return (
-
+
{timeAgoInSeconds === 1 ? '1 second' : `${timeAgoInSeconds} seconds`} ago
);
diff --git a/apps/explorer/src/app/components/seconds-ago/seconds-ago.spec.tsx b/apps/explorer/src/app/components/seconds-ago/seconds-ago.spec.tsx
new file mode 100644
index 000000000..86a05ad41
--- /dev/null
+++ b/apps/explorer/src/app/components/seconds-ago/seconds-ago.spec.tsx
@@ -0,0 +1,36 @@
+import { render, screen, act } from '@testing-library/react';
+import { SecondsAgo } from './index';
+
+beforeEach(() => {
+ jest.useFakeTimers();
+});
+
+afterEach(() => {
+ jest.useRealTimers();
+});
+
+describe('Seconds ago', () => {
+ it('should render successfully', () => {
+ const dateInString = new Date().toString();
+ render(
);
+
+ expect(screen.getByTestId('test-seconds-ago')).toBeInTheDocument();
+ });
+
+ it('should show the correct amount of seconds ago', () => {
+ const secondsToWait = 10;
+ const dateInString = new Date().toString();
+
+ act(() => {
+ jest.advanceTimersByTime(secondsToWait * 1000);
+ });
+
+ jest.runOnlyPendingTimers();
+
+ render(
);
+
+ expect(screen.getByTestId('test-seconds-ago')).toHaveTextContent(
+ `${secondsToWait} seconds ago`
+ );
+ });
+});
diff --git a/apps/explorer/src/app/components/status-message/index.tsx b/apps/explorer/src/app/components/status-message/index.tsx
index 10bc441cc..ae8958270 100644
--- a/apps/explorer/src/app/components/status-message/index.tsx
+++ b/apps/explorer/src/app/components/status-message/index.tsx
@@ -6,7 +6,15 @@ interface StatusMessageProps {
className?: string;
}
-export const StatusMessage = ({ children, className }: StatusMessageProps) => {
+export const StatusMessage = ({
+ children,
+ className,
+ ...props
+}: StatusMessageProps) => {
const classes = classnames('font-alpha text-h4 mb-28', className);
- return
{children}
;
+ return (
+
+ {children}
+
+ );
};
diff --git a/apps/explorer/src/app/components/status-message/status-message.spec.tsx b/apps/explorer/src/app/components/status-message/status-message.spec.tsx
new file mode 100644
index 000000000..541b10fbe
--- /dev/null
+++ b/apps/explorer/src/app/components/status-message/status-message.spec.tsx
@@ -0,0 +1,11 @@
+import { render, screen } from '@testing-library/react';
+import { StatusMessage } from './index';
+
+describe('Status message', () => {
+ it('should render successfully', () => {
+ render(
+
test
+ );
+ expect(screen.getByTestId('status-message-test')).toBeInTheDocument();
+ });
+});
diff --git a/apps/explorer/src/app/components/table/table.spec.tsx b/apps/explorer/src/app/components/table/table.spec.tsx
new file mode 100644
index 000000000..9ba2e5bd8
--- /dev/null
+++ b/apps/explorer/src/app/components/table/table.spec.tsx
@@ -0,0 +1,79 @@
+import { render, screen } from '@testing-library/react';
+
+import { Table, TableRow, TableHeader, TableCell } from './index';
+
+describe('Renders all table components', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('test-table')).toBeInTheDocument();
+ expect(screen.getByTestId('test-tr')).toBeInTheDocument();
+ expect(screen.getByTestId('test-th')).toHaveTextContent('Title');
+ expect(screen.getByTestId('test-td')).toHaveTextContent('Content');
+});
+
+describe('Table row', () => {
+ it('should include classes based on custom "modifier" prop', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('modifier-test')).toHaveClass('border-white-40');
+ });
+});
+
+describe('Table header', () => {
+ it('should accept props i.e. scope="row"', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('props-test')).toHaveAttribute('scope');
+ });
+
+ it('should include custom class based on scope="row"', () => {
+ render(
+
+
+
+ With scope attribute
+
+
+
+ );
+
+ expect(screen.getByTestId('scope-class-test')).toHaveClass('text-left');
+ });
+});
+
+describe('Table cell', () => {
+ it('should include class based on custom "modifier" prop', () => {
+ render(
+
+
+
+ With modifier
+
+
+
+ );
+
+ expect(screen.getByTestId('modifier-class-test')).toHaveClass('py-4');
+ });
+});
diff --git a/apps/explorer/src/app/components/truncate/truncate.spec.tsx b/apps/explorer/src/app/components/truncate/truncate.spec.tsx
new file mode 100644
index 000000000..85677d8be
--- /dev/null
+++ b/apps/explorer/src/app/components/truncate/truncate.spec.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from '@testing-library/react';
+import { TruncateInline } from './truncate';
+
+describe('Truncate', () => {
+ it('should render successfully', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('truncate-test')).toBeInTheDocument();
+ });
+
+ it('it truncates as expected', () => {
+ const test = 'randomstringblahblah';
+ const startChars = 3;
+ const endChars = 3;
+ const expectedString = `${test.slice(0, startChars)}…${test.slice(
+ -endChars
+ )}`;
+
+ render(
+
+ );
+
+ expect(screen.getByText(expectedString)).toBeInTheDocument();
+ });
+
+ it("it doesn't truncate if the string is too short", () => {
+ const test = 'randomstringblahblah';
+ const startChars = test.length;
+ const endChars = test.length;
+
+ render(
+
+ );
+
+ expect(screen.getByText(test)).toBeInTheDocument();
+ });
+});
diff --git a/apps/explorer/src/app/components/truncate/truncate.tsx b/apps/explorer/src/app/components/truncate/truncate.tsx
index 44cf2e2ba..c478c8fe4 100644
--- a/apps/explorer/src/app/components/truncate/truncate.tsx
+++ b/apps/explorer/src/app/components/truncate/truncate.tsx
@@ -22,6 +22,7 @@ export function TruncateInline({
children,
startChars,
endChars,
+ ...props
}: TruncateInlineProps) {
if (text === null) {
return
;
@@ -31,6 +32,7 @@ export function TruncateInline({
const wrapperProps = {
title: text,
className,
+ ...props,
};
if (children !== undefined) {
diff --git a/apps/explorer/src/app/components/txs/id/tx-content.tsx b/apps/explorer/src/app/components/txs/id/tx-content.tsx
index ed0f0112e..e0ecc669d 100644
--- a/apps/explorer/src/app/components/txs/id/tx-content.tsx
+++ b/apps/explorer/src/app/components/txs/id/tx-content.tsx
@@ -1,6 +1,8 @@
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
-import { Table } from '../../table';
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;
@@ -8,29 +10,26 @@ interface TxContentProps {
export const TxContent = ({ data }: TxContentProps) => {
if (!data?.Command) {
- return <>Awaiting decoded transaction data>;
+ return (
+
Could not retrieve transaction content
+ );
}
- const { marketId, type, side, size } = JSON.parse(data.Command);
-
- const displayCode = {
- market: marketId,
- type,
- side,
- size,
- };
-
return (
<>
-
-
- Type |
- {data.Type} |
-
+
- Decoded transaction content
-
+ Decoded transaction content
+
>
);
};
diff --git a/apps/explorer/src/app/components/txs/id/tx-details.tsx b/apps/explorer/src/app/components/txs/id/tx-details.tsx
index 2d674ba79..71a9f1915 100644
--- a/apps/explorer/src/app/components/txs/id/tx-details.tsx
+++ b/apps/explorer/src/app/components/txs/id/tx-details.tsx
@@ -1,50 +1,63 @@
import { Link } from 'react-router-dom';
import { Routes } from '../../../routes/router-config';
import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
-import { Table, TableRow, TableCell } from '../../table';
+import { Table, TableRow, TableCell, TableHeader } from '../../table';
+import { TruncateInline } from '../../truncate/truncate';
interface TxDetailsProps {
txData: Result | undefined;
pubKey: string | undefined;
+ className?: string;
}
-export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
+const truncateLength = 30;
+
+export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
if (!txData) {
return <>Awaiting Tendermint transaction details>;
}
return (
-
-
+
+
Hash
- {txData.hash}
+
+ {txData.hash}
+
- {pubKey ? (
-
- Submitted by |
-
- {pubKey}
- |
-
- ) : (
-
- Submitted by |
- Awaiting decoded transaction data |
-
- )}
- {txData.height ? (
-
- Block |
-
-
- {txData.height}
-
- |
-
- ) : null}
-
- Encoded tnx |
- {txData.tx} |
+
+
+ Submitted by
+
+
+
+ {pubKey}
+
+
+
+
+ Block
+
+
+ {txData.height}
+
+
+
+
+ Encoded tnx
+
+
+
);
diff --git a/apps/explorer/src/app/components/txs/tx-order-type/index.tsx b/apps/explorer/src/app/components/txs/tx-order-type/index.tsx
new file mode 100644
index 000000000..4df94080a
--- /dev/null
+++ b/apps/explorer/src/app/components/txs/tx-order-type/index.tsx
@@ -0,0 +1,43 @@
+import { Lozenge } from '@vegaprotocol/ui-toolkit';
+
+interface TxOrderTypeProps {
+ orderType: string;
+ className?: string;
+}
+
+interface StringMap {
+ [key: string]: string;
+}
+
+// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
+const displayString: StringMap = {
+ OrderSubmission: 'Order Submission',
+ OrderCancellation: 'Order Cancellation',
+ OrderAmendment: 'Order Amendment',
+ VoteSubmission: 'Vote Submission',
+ WithdrawSubmission: 'Withdraw Submission',
+ LiquidityProvisionSubmission: 'Liquidity Provision',
+ LiquidityProvisionCancellation: 'Liquidity Cancellation',
+ LiquidityProvisionAmendment: 'Liquidity Amendment',
+ ProposalSubmission: 'Governance Proposal',
+ AnnounceNode: 'Node Announcement',
+ NodeVote: 'Node Vote',
+ NodeSignature: 'Node Signature',
+ ChainEvent: 'Chain Event',
+ OracleDataSubmission: 'Oracle Data',
+ DelegateSubmission: 'Delegation',
+ UndelegateSubmission: 'Undelegation',
+ KeyRotateSubmission: 'Key Rotation',
+ StateVariableProposal: 'State Variable Proposal',
+ Transfer: 'Transfer',
+ CancelTransfer: 'Cancel Transfer',
+ ValidatorHeartbeat: 'Validator Heartbeat',
+};
+
+export const TxOrderType = ({ orderType, className }: TxOrderTypeProps) => {
+ return (
+
+ {displayString[orderType] || orderType}
+
+ );
+};
diff --git a/apps/explorer/src/app/components/txs/txs-per-block/index.tsx b/apps/explorer/src/app/components/txs/txs-per-block/index.tsx
index 07af94457..d4e4b5555 100644
--- a/apps/explorer/src/app/components/txs/txs-per-block/index.tsx
+++ b/apps/explorer/src/app/components/txs/txs-per-block/index.tsx
@@ -5,6 +5,7 @@ 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';
interface TxsPerBlockProps {
blockHeight: string | undefined;
@@ -62,7 +63,9 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
className="font-mono"
/>
- {Type} |
+
+
+ |
);
})}
diff --git a/apps/explorer/src/app/routes/txs/id/index.tsx b/apps/explorer/src/app/routes/txs/id/index.tsx
index ee24647b2..5698bdbe4 100644
--- a/apps/explorer/src/app/routes/txs/id/index.tsx
+++ b/apps/explorer/src/app/routes/txs/id/index.tsx
@@ -1,20 +1,24 @@
import React from 'react';
import { useParams } from 'react-router-dom';
-import { TxContent, TxDetails } from '../../../components/txs';
-import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
-import { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
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';
const Tx = () => {
const { txHash } = useParams<{ txHash: string }>();
+
const {
- state: { data: transactionData },
+ state: { data: tTxData, loading: tTxLoading, error: tTxError },
} = useFetch(
`${DATA_SOURCES.tendermintUrl}/tx?hash=${txHash}`
);
+
const {
- state: { data: decodedData },
+ state: { data: ceTxData, loading: ceTxLoading, error: ceTxError },
} = useFetch(DATA_SOURCES.chainExplorerUrl, {
method: 'POST',
body: JSON.stringify({
@@ -25,13 +29,20 @@ const Tx = () => {
return (
- Transaction details
-
- Transaction content
-
+ Transaction details
+
+
+
+
+
+ Transaction content
+
+
+
);
};
diff --git a/libs/tailwindcss-config/src/theme.js b/libs/tailwindcss-config/src/theme.js
index d093df574..2602a06f8 100644
--- a/libs/tailwindcss-config/src/theme.js
+++ b/libs/tailwindcss-config/src/theme.js
@@ -46,6 +46,7 @@ module.exports = {
prompt: '#EDFF22',
success: '#26FF8A',
help: '#494949',
+ highlight: '#E5E5E5',
},
'intent-background': {
danger: '#9E0025', // for white text
@@ -93,6 +94,14 @@ module.exports = {
1: '1px',
4: '4px',
},
+ borderRadius: {
+ none: '0',
+ sm: '0.125rem',
+ DEFAULT: '0.225rem',
+ md: '0.3rem',
+ lg: '0.5rem',
+ full: '9999px',
+ },
fontFamily: {
mono: defaultTheme.fontFamily.mono,
serif: defaultTheme.fontFamily.serif,
diff --git a/libs/ui-toolkit/src/components/lozenge/index.ts b/libs/ui-toolkit/src/components/lozenge/index.ts
new file mode 100644
index 000000000..df4ba1467
--- /dev/null
+++ b/libs/ui-toolkit/src/components/lozenge/index.ts
@@ -0,0 +1 @@
+export * from './lozenge';
diff --git a/libs/ui-toolkit/src/components/lozenge/lozenge.spec.tsx b/libs/ui-toolkit/src/components/lozenge/lozenge.spec.tsx
new file mode 100644
index 000000000..6da383483
--- /dev/null
+++ b/libs/ui-toolkit/src/components/lozenge/lozenge.spec.tsx
@@ -0,0 +1,10 @@
+import { render } from '@testing-library/react';
+
+import { Lozenge } from './lozenge';
+
+describe('Lozenge', () => {
+ it('should render successfully', () => {
+ const { baseElement } = render(Lozenge);
+ expect(baseElement).toBeTruthy();
+ });
+});
diff --git a/libs/ui-toolkit/src/components/lozenge/lozenge.stories.tsx b/libs/ui-toolkit/src/components/lozenge/lozenge.stories.tsx
new file mode 100644
index 000000000..6ceefccf6
--- /dev/null
+++ b/libs/ui-toolkit/src/components/lozenge/lozenge.stories.tsx
@@ -0,0 +1,36 @@
+import { Story, Meta } from '@storybook/react';
+import { Lozenge } from './lozenge';
+
+export default {
+ component: Lozenge,
+ title: 'Lozenge',
+} as Meta;
+
+const Template: Story = (args) => lozenge;
+
+export const Default = Template.bind({});
+
+export const WithDetails = Template.bind({});
+WithDetails.args = {
+ details: 'details text',
+};
+
+export const Highlight = Template.bind({});
+Highlight.args = {
+ variant: 'highlight',
+};
+
+export const Success = Template.bind({});
+Success.args = {
+ variant: 'success',
+};
+
+export const Warning = Template.bind({});
+Warning.args = {
+ variant: 'warning',
+};
+
+export const Danger = Template.bind({});
+Danger.args = {
+ variant: 'danger',
+};
diff --git a/libs/ui-toolkit/src/components/lozenge/lozenge.tsx b/libs/ui-toolkit/src/components/lozenge/lozenge.tsx
new file mode 100644
index 000000000..e12fd4020
--- /dev/null
+++ b/libs/ui-toolkit/src/components/lozenge/lozenge.tsx
@@ -0,0 +1,38 @@
+import { ReactNode } from 'react';
+import classNames from 'classnames';
+
+interface LozengeProps {
+ children: ReactNode;
+ variant?: 'success' | 'warning' | 'danger' | 'highlight';
+ className?: string;
+ details?: string;
+}
+
+const getWrapperClasses = (className: LozengeProps['className']) => {
+ return classNames('inline-flex items-center gap-4', className);
+};
+
+const getLozengeClasses = (variant: LozengeProps['variant']) => {
+ return classNames(['rounded-md', 'font-mono', 'leading-none', 'p-4'], {
+ 'bg-intent-success text-black': variant === 'success',
+ 'bg-intent-danger text-white': variant === 'danger',
+ 'bg-intent-warning text-black': variant === 'warning',
+ 'bg-intent-highlight text-black': variant === 'highlight',
+ 'bg-intent-help text-white': !variant,
+ });
+};
+
+export const Lozenge = ({
+ children,
+ variant,
+ className,
+ details,
+}: LozengeProps) => {
+ return (
+
+ {children}
+
+ {details && {details}}
+
+ );
+};
diff --git a/libs/ui-toolkit/src/index.ts b/libs/ui-toolkit/src/index.ts
index 47e9f56b7..638072c6b 100644
--- a/libs/ui-toolkit/src/index.ts
+++ b/libs/ui-toolkit/src/index.ts
@@ -10,6 +10,7 @@ export { FormGroup } from './components/form-group';
export { Icon } from './components/icon';
export { Input } from './components/input';
export { InputError } from './components/input-error';
+export { Lozenge } from './components/lozenge';
export { Loader } from './components/loader';
export { Select } from './components/select';
export { Splash } from './components/splash';