Philipp Hoenisch
3 years ago
6 changed files with 604 additions and 35 deletions
@ -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> |
|||
</> |
|||
); |
|||
} |
@ -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} |
|||
</> |
|||
); |
|||
} |
Loading…
Reference in new issue