Browse Source

Add table for rendering CFDs

fix-bad-api-calls
Philipp Hoenisch 3 years ago
parent
commit
95713fc4cd
No known key found for this signature in database GPG Key ID: E5F8E74C672BC666
  1. 4
      frontend/package.json
  2. 68
      frontend/src/Maker.tsx
  3. 62
      frontend/src/Taker.tsx
  4. 259
      frontend/src/components/cfdtables/CfdTable.tsx
  5. 236
      frontend/src/components/cfdtables/CfdTableMaker.tsx
  6. 10
      frontend/yarn.lock

4
frontend/package.json

@ -21,7 +21,6 @@
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-table": "^7.7.2",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"babel-eslint": "^10.1.0",
@ -44,13 +43,14 @@
"react-router-dom": "=6.0.0-beta.2",
"react-scripts": "^4.0.3",
"react-sse-hooks": "^1.0.5",
"react-table": "^7.7.0",
"react-table": "7.7.0",
"typescript": "^4.4.2",
"vite-jest": "^0.0.3",
"web-vitals": "^1.0.1"
},
"devDependencies": {
"@types/eslint": "^7",
"@types/react-table": "7.7.3",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react-refresh": "^1.3.1",

68
frontend/src/Maker.tsx

@ -1,8 +1,24 @@
import { Button, Container, Flex, Grid, GridItem, HStack, Stack, Text, useToast, VStack } from "@chakra-ui/react";
import {
Button,
Container,
Flex,
Grid,
GridItem,
HStack,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useToast,
VStack,
} from "@chakra-ui/react";
import React, { useState } from "react";
import { useAsync } from "react-async";
import { useEventSource } from "react-sse-hooks";
import CfdTile from "./components/CfdTile";
import { CfdTable } from "./components/cfdtables/CfdTable";
import { CfdTableMaker } from "./components/cfdtables/CfdTableMaker";
import CurrencyInputField from "./components/CurrencyInputField";
import useLatestEvent from "./components/Hooks";
import OrderTile from "./components/OrderTile";
@ -13,7 +29,8 @@ import { CfdSellOrderPayload, postCfdSellOrderRequest } from "./MakerClient";
export default function App() {
let source = useEventSource({ source: "/api/feed", options: { withCredentials: true } });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const cfdsOrUndefined = useLatestEvent<Cfd[]>(source, "cfds");
let cfds = cfdsOrUndefined ? cfdsOrUndefined! : [];
const order = useLatestEvent<Order>(source, "order");
console.log(cfds);
@ -46,20 +63,20 @@ export default function App() {
},
});
const runningStates = ["Accepted", "Contract Setup", "Pending Open"];
const running = cfds.filter((value) => runningStates.includes(value.state));
const openStates = ["Requested"];
const open = cfds.filter((value) => openStates.includes(value.state));
const closedStates = ["Rejected", "Closed"];
const closed = cfds.filter((value) => closedStates.includes(value.state));
// TODO: remove this. It just helps to detect immediately if we missed a state.
const unsorted = cfds.filter((value) =>
!runningStates.includes(value.state) && !closedStates.includes(value.state) && !openStates.includes(value.state)
);
return (
<Container maxWidth="120ch" marginTop="1rem">
<Grid templateColumns="repeat(6, 1fr)" gap={4}>
<GridItem colSpan={4}>
<Stack>
{cfds && cfds.map((cfd, index) =>
<CfdTile
key={"cfd_" + index}
index={index}
cfd={cfd}
/>
)}
</Stack>
</GridItem>
<GridItem colStart={5} colSpan={2}>
<Wallet walletInfo={walletInfo} />
<VStack spacing={5} shadow={"md"} padding={5} align={"stretch"}>
@ -115,6 +132,29 @@ export default function App() {
</VStack>
</GridItem>
</Grid>
<Tabs>
<TabList>
<Tab>Running [{running.length}]</Tab>
<Tab>Open [{open.length}]</Tab>
<Tab>Closed [{closed.length}]</Tab>
<Tab>Unsorted [{unsorted.length}] (should be empty)</Tab>
</TabList>
<TabPanels>
<TabPanel>
<CfdTable data={running} />
</TabPanel>
<TabPanel>
<CfdTableMaker data={open} />
</TabPanel>
<TabPanel>
<CfdTable data={closed} />
</TabPanel>
<TabPanel>
<CfdTable data={unsorted} />
</TabPanel>
</TabPanels>
</Tabs>
</Container>
);
}

62
frontend/src/Taker.tsx

@ -1,8 +1,24 @@
import { Button, Container, Flex, Grid, GridItem, HStack, Stack, Text, useToast, VStack } from "@chakra-ui/react";
import {
Button,
Container,
Flex,
Grid,
GridItem,
HStack,
Stack,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useToast,
VStack,
} from "@chakra-ui/react";
import React, { useState } from "react";
import { useAsync } from "react-async";
import { useEventSource } from "react-sse-hooks";
import CfdTile from "./components/CfdTile";
import { CfdTable } from "./components/cfdtables/CfdTable";
import CurrencyInputField from "./components/CurrencyInputField";
import useLatestEvent from "./components/Hooks";
import { Cfd, Order, WalletInfo } from "./components/Types";
@ -44,7 +60,8 @@ async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse>
export default function App() {
let source = useEventSource({ source: "/api/feed" });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const cfdsOrUndefined = useLatestEvent<Cfd[]>(source, "cfds");
let cfds = cfdsOrUndefined ? cfdsOrUndefined! : [];
const order = useLatestEvent<Order>(source, "order");
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet");
@ -92,20 +109,18 @@ export default function App() {
},
});
const runningStates = ["Request sent", "Requested", "Contract Setup", "Pending Open"];
const running = cfds.filter((value) => runningStates.includes(value.state));
const closedStates = ["Rejected", "Closed"];
const closed = cfds.filter((value) => closedStates.includes(value.state));
// TODO: remove this. It just helps to detect immediately if we missed a state.
const unsorted = cfds.filter((value) =>
!runningStates.includes(value.state) && !closedStates.includes(value.state)
);
return (
<Container maxWidth="120ch" marginTop="1rem">
<Grid templateColumns="repeat(6, 1fr)" gap={4}>
<GridItem colSpan={4}>
<Stack>
{cfds && cfds.map((cfd, index) =>
<CfdTile
key={"cfd_" + index}
index={index}
cfd={cfd}
/>
)}
</Stack>
</GridItem>
<GridItem colStart={5} colSpan={2}>
<Wallet walletInfo={walletInfo} />
<VStack spacing={5} shadow={"md"} padding={5} align={"stretch"}>
@ -167,6 +182,25 @@ export default function App() {
</VStack>
</GridItem>
</Grid>
<Tabs>
<TabList>
<Tab>Running [{running.length}]</Tab>
<Tab>Closed [{closed.length}]</Tab>
<Tab>Unsorted [{unsorted.length}] (should be empty)</Tab>
</TabList>
<TabPanels>
<TabPanel>
<CfdTable data={running} />
</TabPanel>
<TabPanel>
<CfdTable data={closed} />
</TabPanel>
<TabPanel>
<CfdTable data={unsorted} />
</TabPanel>
</TabPanels>
</Tabs>
</Container>
);
}

259
frontend/src/components/cfdtables/CfdTable.tsx

@ -0,0 +1,259 @@
import { ChevronRightIcon, ChevronUpIcon, TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons";
import { Badge, Box, chakra, HStack, IconButton, Table as CUITable, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
import React from "react";
import { Column, Row, useExpanded, useSortBy, useTable } from "react-table";
import { Cfd } from "../Types";
interface CfdTableProps {
data: Cfd[];
}
export function CfdTable(
{ data }: CfdTableProps,
) {
const tableData = React.useMemo(
() => data,
[data],
);
const columns: Array<Column<Cfd>> = React.useMemo(
() => [
{
id: "expander",
Header: () => null,
Cell: ({ row }: any) => (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded
? <IconButton
aria-label="Reduce"
icon={<ChevronUpIcon />}
onClick={() => {
row.toggleRowExpanded();
}}
/>
: <IconButton
aria-label="Expand"
icon={<ChevronRightIcon />}
onClick={() => {
row.toggleRowExpanded();
}}
/>}
</span>
),
},
{
Header: "OrderId",
accessor: "order_id", // accessor is the "key" in the data
},
{
Header: "Position",
accessor: ({ position }) => {
let colorScheme = "green";
if (position.toLocaleLowerCase() === "buy") {
colorScheme = "purple";
}
return (
<Badge colorScheme={colorScheme}>{position}</Badge>
);
},
isNumeric: true,
},
{
Header: "Quantity",
accessor: ({ quantity_usd }) => {
return (<Dollars amount={quantity_usd} />);
},
isNumeric: true,
},
{
Header: "Leverage",
accessor: "leverage",
isNumeric: true,
},
{
Header: "Margin",
accessor: "margin",
isNumeric: true,
},
{
Header: "Initial Price",
accessor: ({ initial_price }) => {
return (<Dollars amount={initial_price} />);
},
isNumeric: true,
},
{
Header: "Liquidation Price",
isNumeric: true,
accessor: ({ liquidation_price }) => {
return (<Dollars amount={liquidation_price} />);
},
},
{
Header: "Unrealized P/L",
accessor: ({ profit_usd }) => {
return (<Dollars amount={profit_usd} />);
},
isNumeric: true,
},
{
Header: "Timestamp",
accessor: "state_transition_timestamp",
},
{
Header: "State",
accessor: ({ state }) => {
let colorScheme = "gray";
if (state.toLowerCase() === "rejected") {
colorScheme = "red";
}
if (state.toLowerCase() === "contract setup") {
colorScheme = "green";
}
return (
<Badge colorScheme={colorScheme}>{state}</Badge>
);
},
},
],
[],
);
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp"];
return (
<Table
tableData={tableData}
columns={columns}
hiddenColumns={hiddenColumns}
renderDetails={renderRowSubComponent}
/>
);
}
function renderRowSubComponent(row: Row<Cfd>) {
// TODO: I would show additional information here such as txids, timestamps, actions
let cells = row.allCells
.filter((cell) => {
return ["state_transition_timestamp"].includes(cell.column.id);
})
.map((cell) => {
return cell;
});
return (
<>
Showing some more information here...
<HStack>
{cells.map(cell => (
<Box key={cell.column.id}>
{cell.column.id} = {cell.render("Cell")}
</Box>
))}
</HStack>
</>
);
}
interface DollarsProps {
amount: number;
}
function Dollars({ amount }: DollarsProps) {
const price = Math.floor(amount * 100.0) / 100.0;
return (
<>
$ {price}
</>
);
}
interface TableProps {
columns: Array<Column<Cfd>>;
tableData: Cfd[];
hiddenColumns: string[];
renderDetails: any;
}
export function Table({ columns, tableData, hiddenColumns, renderDetails }: TableProps) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
visibleColumns,
} = useTable(
{
columns,
data: tableData,
initialState: {
hiddenColumns,
},
},
useSortBy,
useExpanded,
);
return (
<>
<CUITable {...getTableProps()} colorScheme="blue">
<Thead>
{headerGroups.map((headerGroup) => (
<Tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<Th
{...column.getHeaderProps(column.getSortByToggleProps())}
isNumeric={column.isNumeric}
>
{column.render("Header")}
<chakra.span pl="4">
{column.isSorted
? (
column.isSortedDesc
? (
<TriangleDownIcon aria-label="sorted descending" />
)
: (
<TriangleUpIcon aria-label="sorted ascending" />
)
)
: null}
</chakra.span>
</Th>
))}
</Tr>
))}
</Thead>
<Tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<React.Fragment key={row.id}>
<Tr {...row.getRowProps()} onClick={() => row.toggleRowExpanded()}>
{row.cells.map((cell) => (
<Td {...cell.getCellProps()} isNumeric={cell.column.isNumeric}>
{cell.render("Cell")}
</Td>
))}
</Tr>
{row.isExpanded
? (
<Tr>
<Td>
</Td>
<Td colSpan={visibleColumns.length - 1}>
{renderDetails(row)}
</Td>
</Tr>
)
: null}
</React.Fragment>
);
})}
</Tbody>
</CUITable>
</>
);
}

236
frontend/src/components/cfdtables/CfdTableMaker.tsx

@ -0,0 +1,236 @@
import { CheckIcon, ChevronRightIcon, ChevronUpIcon, CloseIcon } from "@chakra-ui/icons";
import { Badge, Box, HStack, IconButton, useToast } from "@chakra-ui/react";
import React from "react";
import { useAsync } from "react-async";
import { Column, Row } from "react-table";
import { postAcceptOrder, postRejectOrder } from "../../MakerClient";
import { Cfd } from "../Types";
import { Table } from "./CfdTable";
interface CfdMableTakerProps {
data: Cfd[];
}
export function CfdTableMaker(
{ data }: CfdMableTakerProps,
) {
const tableData = React.useMemo(
() => data,
[data],
);
const toast = useToast();
let { run: acceptOrder, isLoading: isAccepting } = useAsync({
deferFn: async ([orderId]: any[]) => {
try {
let payload = {
order_id: orderId,
};
await postAcceptOrder(payload);
} catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
}
},
});
let { run: rejectOrder, isLoading: isRejecting } = useAsync({
deferFn: async ([orderId]: any[]) => {
try {
let payload = {
order_id: orderId,
};
await postRejectOrder(payload);
} catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
}
},
});
const columns: Array<Column<Cfd>> = React.useMemo(
() => [
{
id: "expander",
Header: () => null,
Cell: ({ row }: any) => (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded
? <IconButton
aria-label="Reduce"
icon={<ChevronUpIcon />}
onClick={() => {
row.toggleRowExpanded();
}}
/>
: <IconButton
aria-label="Expand"
icon={<ChevronRightIcon />}
onClick={() => {
row.toggleRowExpanded();
}}
/>}
</span>
),
},
{
Header: "OrderId",
accessor: "order_id",
},
{
Header: "Position",
accessor: ({ position }) => {
let colorScheme = "green";
if (position.toLocaleLowerCase() === "buy") {
colorScheme = "purple";
}
return (
<Badge colorScheme={colorScheme}>{position}</Badge>
);
},
isNumeric: true,
},
{
Header: "Quantity",
accessor: ({ quantity_usd }) => {
return (<Dollars amount={quantity_usd} />);
},
isNumeric: true,
},
{
Header: "Leverage",
accessor: "leverage",
isNumeric: true,
},
{
Header: "Margin",
accessor: "margin",
isNumeric: true,
},
{
Header: "Initial Price",
accessor: ({ initial_price }) => {
return (<Dollars amount={initial_price} />);
},
isNumeric: true,
},
{
Header: "Liquidation Price",
isNumeric: true,
accessor: ({ liquidation_price }) => {
return (<Dollars amount={liquidation_price} />);
},
},
{
Header: "Unrealized P/L",
accessor: ({ profit_usd }) => {
return (<Dollars amount={profit_usd} />);
},
isNumeric: true,
},
{
Header: "Timestamp",
accessor: "state_transition_timestamp",
},
{
Header: "Action",
accessor: ({ state, order_id }) => {
if (state.toLowerCase() === "requested") {
return (<HStack>
<IconButton
colorScheme="green"
aria-label="Accept"
icon={<CheckIcon />}
onClick={async () => acceptOrder(order_id)}
isLoading={isAccepting}
/>
<IconButton
colorScheme="red"
aria-label="Reject"
icon={<CloseIcon />}
onClick={async () => rejectOrder(order_id)}
isLoading={isRejecting}
/>
</HStack>);
}
let colorScheme = "gray";
if (state.toLowerCase() === "rejected") {
colorScheme = "red";
}
if (state.toLowerCase() === "contract setup") {
colorScheme = "green";
}
return (
<Badge colorScheme={colorScheme}>{state}</Badge>
);
},
},
],
[],
);
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
const hiddenColumns = ["order_id", "leverage", "Unrealized P/L", "state_transition_timestamp"];
return (
<Table
tableData={tableData}
columns={columns}
hiddenColumns={hiddenColumns}
renderDetails={renderRowSubComponent}
/>
);
}
function renderRowSubComponent(row: Row<Cfd>) {
// TODO: I would show additional information here such as txids, timestamps, actions
let cells = row.allCells
.filter((cell) => {
return ["state_transition_timestamp"].includes(cell.column.id);
})
.map((cell) => {
return cell;
});
return (
<>
Showing some more information here...
<HStack>
{cells.map(cell => (
<Box key={cell.column.id}>
{cell.column.id} = {cell.render("Cell")}
</Box>
))}
</HStack>
</>
);
}
interface DollarsProps {
amount: number;
}
function Dollars({ amount }: DollarsProps) {
const price = Math.floor(amount * 100.0) / 100.0;
return (
<>
$ {price}
</>
);
}

10
frontend/yarn.lock

@ -2800,10 +2800,10 @@
dependencies:
"@types/react" "*"
"@types/react-table@^7.7.2":
version "7.7.2"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.2.tgz#434f8230eb011c7eed8f3550fdf25befafebcfac"
integrity sha512-NwB78t3YV5pZ1NK3m2vylb/d0DKVyWH4y4GMCtlE4tg2n5ENM4ejzKnT46YKuqG2cPjWc+PIxuRVMd5OYX1z4A==
"@types/react-table@7.7.3":
version "7.7.3"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.3.tgz#0e5f952ec8562db1f6c950c766b53f27294a7e89"
integrity sha512-IL9DsA+V9AXUSPT6L+fFjo6YfEV40Fb+WmbrVxn+TjsPYUjkMZ0EZP1q0lTiosdrbrq3TeQI35Naxqc0ZTWEQg==
dependencies:
"@types/react" "*"
@ -10833,7 +10833,7 @@ react-style-singleton@^2.1.0:
invariant "^2.2.4"
tslib "^1.0.0"
react-table@^7.7.0:
react-table@7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912"
integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==

Loading…
Cancel
Save