diff --git a/frontend/package.json b/frontend/package.json index c992357..b4e93c6 100644 --- a/frontend/package.json +++ b/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", diff --git a/frontend/src/Maker.tsx b/frontend/src/Maker.tsx index 5237bd9..0956881 100644 --- a/frontend/src/Maker.tsx +++ b/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(source, "cfds"); + const cfdsOrUndefined = useLatestEvent(source, "cfds"); + let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; const order = useLatestEvent(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 ( - - - {cfds && cfds.map((cfd, index) => - - )} - - @@ -115,6 +132,29 @@ export default function App() { + + + Running [{running.length}] + Open [{open.length}] + Closed [{closed.length}] + Unsorted [{unsorted.length}] (should be empty) + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/Taker.tsx b/frontend/src/Taker.tsx index a4998ca..38d8eaf 100644 --- a/frontend/src/Taker.tsx +++ b/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 export default function App() { let source = useEventSource({ source: "/api/feed" }); - const cfds = useLatestEvent(source, "cfds"); + const cfdsOrUndefined = useLatestEvent(source, "cfds"); + let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; const order = useLatestEvent(source, "order"); const walletInfo = useLatestEvent(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 ( - - - {cfds && cfds.map((cfd, index) => - - )} - - @@ -167,6 +182,25 @@ export default function App() { + + + Running [{running.length}] + Closed [{closed.length}] + Unsorted [{unsorted.length}] (should be empty) + + + + + + + + + + + + + + ); } diff --git a/frontend/src/components/cfdtables/CfdTable.tsx b/frontend/src/components/cfdtables/CfdTable.tsx new file mode 100644 index 0000000..f64c7dd --- /dev/null +++ b/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> = React.useMemo( + () => [ + { + id: "expander", + Header: () => null, + Cell: ({ row }: any) => ( + + {row.isExpanded + ? } + onClick={() => { + row.toggleRowExpanded(); + }} + /> + : } + onClick={() => { + row.toggleRowExpanded(); + }} + />} + + ), + }, + { + 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 ( + {position} + ); + }, + isNumeric: true, + }, + + { + Header: "Quantity", + accessor: ({ quantity_usd }) => { + return (); + }, + isNumeric: true, + }, + { + Header: "Leverage", + accessor: "leverage", + isNumeric: true, + }, + { + Header: "Margin", + accessor: "margin", + isNumeric: true, + }, + { + Header: "Initial Price", + accessor: ({ initial_price }) => { + return (); + }, + isNumeric: true, + }, + { + Header: "Liquidation Price", + isNumeric: true, + accessor: ({ liquidation_price }) => { + return (); + }, + }, + { + Header: "Unrealized P/L", + accessor: ({ profit_usd }) => { + return (); + }, + 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 ( + {state} + ); + }, + }, + ], + [], + ); + + // 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 ( + + ); +} + +function renderRowSubComponent(row: Row) { + // 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... + + {cells.map(cell => ( + + {cell.column.id} = {cell.render("Cell")} + + ))} + + + ); +} + +interface DollarsProps { + amount: number; +} +function Dollars({ amount }: DollarsProps) { + const price = Math.floor(amount * 100.0) / 100.0; + return ( + <> + $ {price} + + ); +} + +interface TableProps { + columns: Array>; + 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 ( + <> + + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + + {rows.map((row) => { + prepareRow(row); + return ( + + row.toggleRowExpanded()}> + {row.cells.map((cell) => ( + + ))} + + {row.isExpanded + ? ( + + + + + ) + : null} + + ); + })} + + + + ); +} diff --git a/frontend/src/components/cfdtables/CfdTableMaker.tsx b/frontend/src/components/cfdtables/CfdTableMaker.tsx new file mode 100644 index 0000000..a7951f0 --- /dev/null +++ b/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> = React.useMemo( + () => [ + { + id: "expander", + Header: () => null, + Cell: ({ row }: any) => ( + + {row.isExpanded + ? } + onClick={() => { + row.toggleRowExpanded(); + }} + /> + : } + onClick={() => { + row.toggleRowExpanded(); + }} + />} + + ), + }, + { + Header: "OrderId", + accessor: "order_id", + }, + { + Header: "Position", + accessor: ({ position }) => { + let colorScheme = "green"; + if (position.toLocaleLowerCase() === "buy") { + colorScheme = "purple"; + } + return ( + {position} + ); + }, + isNumeric: true, + }, + + { + Header: "Quantity", + accessor: ({ quantity_usd }) => { + return (); + }, + isNumeric: true, + }, + { + Header: "Leverage", + accessor: "leverage", + isNumeric: true, + }, + { + Header: "Margin", + accessor: "margin", + isNumeric: true, + }, + { + Header: "Initial Price", + accessor: ({ initial_price }) => { + return (); + }, + isNumeric: true, + }, + { + Header: "Liquidation Price", + isNumeric: true, + accessor: ({ liquidation_price }) => { + return (); + }, + }, + { + Header: "Unrealized P/L", + accessor: ({ profit_usd }) => { + return (); + }, + isNumeric: true, + }, + { + Header: "Timestamp", + accessor: "state_transition_timestamp", + }, + { + Header: "Action", + accessor: ({ state, order_id }) => { + if (state.toLowerCase() === "requested") { + return ( + } + onClick={async () => acceptOrder(order_id)} + isLoading={isAccepting} + /> + } + onClick={async () => rejectOrder(order_id)} + isLoading={isRejecting} + /> + ); + } + + let colorScheme = "gray"; + if (state.toLowerCase() === "rejected") { + colorScheme = "red"; + } + if (state.toLowerCase() === "contract setup") { + colorScheme = "green"; + } + return ( + {state} + ); + }, + }, + ], + [], + ); + + // 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 ( +
+ {column.render("Header")} + + {column.isSorted + ? ( + column.isSortedDesc + ? ( + + ) + : ( + + ) + ) + : null} + +
+ {cell.render("Cell")} +
+ + {renderDetails(row)} +
+ ); +} + +function renderRowSubComponent(row: Row) { + // 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... + + {cells.map(cell => ( + + {cell.column.id} = {cell.render("Cell")} + + ))} + + + ); +} + +interface DollarsProps { + amount: number; +} +function Dollars({ amount }: DollarsProps) { + const price = Math.floor(amount * 100.0) / 100.0; + return ( + <> + $ {price} + + ); +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 65989a8..732c141 100644 --- a/frontend/yarn.lock +++ b/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==