robosats/frontend/src/components/BookTable.tsx

672 lines
19 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import {
Box,
2022-09-29 11:53:12 +00:00
Grid,
Dialog,
Typography,
Paper,
Stack,
ListItemButton,
ListItemText,
ListItemAvatar,
useTheme,
CircularProgress,
LinearProgress,
2022-09-29 11:53:12 +00:00
IconButton,
} from '@mui/material';
2022-10-01 12:03:01 +00:00
import { DataGrid, GridPagination } from '@mui/x-data-grid';
import currencyDict from '../../static/assets/currencies.json';
import { Order } from '../models/Order.model';
import FlagWithProps from './FlagWithProps';
import { pn, amountToString } from '../utils/prettyNumbers';
import PaymentText from './PaymentText';
import RobotAvatar from './Robots/RobotAvatar';
import hexToRgb from '../utils/hexToRgb';
import statusBadgeColor from '../utils/statusBadgeColor';
2022-09-29 11:53:12 +00:00
// Icons
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
2022-09-29 11:53:12 +00:00
interface Props {
loading: boolean;
2022-10-01 12:03:01 +00:00
refreshing: boolean;
clickRefresh: () => void;
orders: Order[];
type: number;
currency: number;
maxWidth: number;
maxHeight: number;
2022-09-29 11:53:12 +00:00
fullWidth: number;
fullHeight: number;
defaultFullscreen: boolean;
}
const BookTable = ({
loading,
refreshing,
clickRefresh,
orders,
type,
currency,
maxWidth,
maxHeight,
2022-09-29 11:53:12 +00:00
fullWidth,
fullHeight,
defaultFullscreen,
}: Props): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const history = useHistory();
2022-09-29 11:53:12 +00:00
const [pageSize, setPageSize] = useState(0);
const [fullscreen, setFullscreen] = useState(defaultFullscreen);
// all sizes in 'em'
2022-09-29 11:53:12 +00:00
const fontSize = theme.typography.fontSize;
const verticalHeightFrame = 6.9075;
const verticalHeightRow = 3.25;
const defaultPageSize = Math.max(
2022-09-29 12:49:01 +00:00
Math.floor(
((fullscreen ? fullHeight * 0.875 : maxHeight) - verticalHeightFrame) / verticalHeightRow,
),
1,
);
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
const [useDefaultPageSize, setUseDefaultPageSize] = useState(true);
useEffect(() => {
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
});
const premiumColor = function (baseColor: string, accentColor: string, point: number) {
const baseRGB = hexToRgb(baseColor);
const accentRGB = hexToRgb(accentColor);
const redDiff = accentRGB[0] - baseRGB[0];
const red = baseRGB[0] + redDiff * point;
const greenDiff = accentRGB[1] - baseRGB[1];
const green = baseRGB[1] + greenDiff * point;
const blueDiff = accentRGB[2] - baseRGB[2];
const blue = baseRGB[2] + blueDiff * point;
return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${
0.7 + point * 0.3
})`;
};
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Orders per page:') },
noRowsLabel: t('No rows'),
noResultsOverlayLabel: t('No results found.'),
errorOverlayDefaultLabel: t('An error occurred.'),
toolbarColumns: t('Columns'),
toolbarColumnsLabel: t('Select columns'),
columnsPanelTextFieldLabel: t('Find column'),
columnsPanelTextFieldPlaceholder: t('Column title'),
columnsPanelDragIconLabel: t('Reorder column'),
columnsPanelShowAllButton: t('Show all'),
columnsPanelHideAllButton: t('Hide all'),
filterPanelAddFilter: t('Add filter'),
filterPanelDeleteIconLabel: t('Delete'),
filterPanelLinkOperator: t('Logic operator'),
filterPanelOperators: t('Operator'),
filterPanelOperatorAnd: t('And'),
filterPanelOperatorOr: t('Or'),
filterPanelColumns: t('Columns'),
filterPanelInputLabel: t('Value'),
filterPanelInputPlaceholder: t('Filter value'),
filterOperatorContains: t('contains'),
filterOperatorEquals: t('equals'),
filterOperatorStartsWith: t('starts with'),
filterOperatorEndsWith: t('ends with'),
filterOperatorIs: t('is'),
filterOperatorNot: t('is not'),
filterOperatorAfter: t('is after'),
filterOperatorOnOrAfter: t('is on or after'),
filterOperatorBefore: t('is before'),
filterOperatorOnOrBefore: t('is on or before'),
filterOperatorIsEmpty: t('is empty'),
filterOperatorIsNotEmpty: t('is not empty'),
filterOperatorIsAnyOf: t('is any of'),
filterValueAny: t('any'),
filterValueTrue: t('true'),
filterValueFalse: t('false'),
columnMenuLabel: t('Menu'),
columnMenuShowColumns: t('Show columns'),
columnMenuFilter: t('Filter'),
columnMenuHideColumn: t('Hide'),
columnMenuUnsort: t('Unsort'),
columnMenuSortAsc: t('Sort by ASC'),
columnMenuSortDesc: t('Sort by DESC'),
columnHeaderFiltersLabel: t('Show filters'),
columnHeaderSortIconLabel: t('Sort'),
booleanCellTrueLabel: t('yes'),
booleanCellFalseLabel: t('no'),
};
const robotObj = function (width: number, hide: boolean) {
return {
hide,
field: 'maker_nick',
headerName: t('Robot'),
width: width * fontSize,
renderCell: (params) => {
return (
<ListItemButton style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}>
<ListItemAvatar>
<RobotAvatar
nickname={params.row.maker_nick}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</ListItemAvatar>
<ListItemText primary={params.row.maker_nick} />
</ListItemButton>
);
},
};
};
const robotSmallObj = function (width: number, hide: boolean) {
return {
hide,
field: 'maker_nick',
headerName: t('Robot'),
width: width * fontSize,
renderCell: (params) => {
return (
2022-10-01 12:03:01 +00:00
<div style={{ position: 'relative', left: '-1.64em' }}>
<ListItemButton style={{ cursor: 'pointer' }}>
<RobotAvatar
nickname={params.row.maker_nick}
smooth={true}
style={{ width: '3.215em', height: '3.215em' }}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</ListItemButton>
</div>
);
},
};
};
const typeObj = function (width: number, hide: boolean) {
return {
hide,
field: 'type',
headerName: t('Is'),
width: width * fontSize,
renderCell: (params) => (params.row.type ? t('Seller') : t('Buyer')),
};
};
const amountObj = function (width: number, hide: boolean) {
return {
hide,
field: 'amount',
headerName: t('Amount'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{amountToString(
params.row.amount,
params.row.has_range,
params.row.min_amount,
params.row.max_amount,
)}
</div>
);
},
};
};
const currencyObj = function (width: number, hide: boolean) {
return {
hide,
field: 'currency',
headerName: t('Currency'),
width: width * fontSize,
renderCell: (params) => {
const currencyCode = currencyDict[params.row.currency.toString()];
return (
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{currencyCode + ' '}
<FlagWithProps code={currencyCode} />
</div>
);
},
};
};
const paymentObj = function (width: number, hide: boolean) {
return {
hide,
field: 'payment_method',
headerName: t('Payment Method'),
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
<PaymentText
othersText={t('Others')}
verbose={true}
size={1.7 * fontSize}
text={params.row.payment_method}
/>
</div>
);
},
};
};
const paymentSmallObj = function (width: number, hide: boolean) {
return {
hide,
field: 'payment_icons',
headerName: t('Pay'),
width: width * fontSize,
renderCell: (params) => {
return (
<div
style={{
position: 'relative',
left: '-4px',
cursor: 'pointer',
align: 'center',
}}
>
<PaymentText
othersText={t('Others')}
size={1.3 * fontSize}
text={params.row.payment_method}
/>
</div>
);
},
};
};
const priceObj = function (width: number, hide: boolean) {
return {
hide,
field: 'price',
headerName: t('Price'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
const currencyCode = currencyDict[params.row.currency.toString()];
return (
<div style={{ cursor: 'pointer' }}>{`${pn(params.row.price)} ${currencyCode}/BTC`}</div>
);
},
};
};
const premiumObj = function (width: number, hide: boolean) {
// coloring premium texts based on 4 params:
// Hardcoded: a sell order at 0% is an outstanding premium
// Hardcoded: a buy order at 10% is an outstanding premium
const sellStandardPremium = 10;
const buyOutstandingPremium = 10;
return {
hide,
field: 'premium',
headerName: t('Premium'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
let fontColor = `rgb(0,0,0)`;
if (params.row.type === 0) {
var premiumPoint = params.row.premium / buyOutstandingPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor(
theme.palette.text.primary,
theme.palette.secondary.dark,
premiumPoint,
);
} else {
var premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor(
theme.palette.text.primary,
theme.palette.primary.dark,
premiumPoint,
);
}
const fontWeight = 400 + Math.round(premiumPoint * 5) * 100;
return (
<div style={{ cursor: 'pointer' }}>
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
</Typography>
</div>
);
},
};
};
const timerObj = function (width: number, hide: boolean) {
return {
hide,
field: 'escrow_duration',
headerName: t('Timer'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
const hours = Math.round(params.row.escrow_duration / 3600);
const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60);
return <div style={{ cursor: 'pointer' }}>{hours > 0 ? `${hours}h` : `${minutes}m`}</div>;
},
};
};
const expiryObj = function (width: number, hide: boolean) {
return {
hide,
field: 'expires_at',
headerName: t('Expiry'),
type: 'string',
width: width * fontSize,
renderCell: (params) => {
const expiresAt = new Date(params.row.expires_at);
const timeToExpiry = Math.abs(expiresAt - new Date());
const percent = Math.round((timeToExpiry / (24 * 60 * 60 * 1000)) * 100);
const hours = Math.round(timeToExpiry / (3600 * 1000));
const minutes = Math.round((timeToExpiry - hours * (3600 * 1000)) / 60000);
return (
<Box sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}>
<CircularProgress
value={percent}
color={percent < 15 ? 'error' : percent < 30 ? 'warning' : 'success'}
thickness={0.35 * fontSize}
size={2.5 * fontSize}
variant='determinate'
/>
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant='caption' component='div' color='text.secondary'>
{hours > 0 ? `${hours}h` : `${minutes}m`}
</Typography>
</Box>
</Box>
);
},
};
};
const satoshisObj = function (width: number, hide: boolean) {
return {
hide,
field: 'satoshis_now',
headerName: t('Sats now'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{params.row.satoshis_now > 1000000
? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M`
: `${pn(Math.round(params.row.satoshis_now / 1000))} K`}
</div>
);
},
};
};
const idObj = function (width: number, hide: boolean) {
return {
hide,
field: 'id',
headerName: 'Order ID',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
<Typography variant='caption' color='text.secondary'>
{`#${params.row.id}`}
</Typography>
</div>
);
},
};
};
const columnSpecs = {
amount: {
priority: 1,
order: 4,
normal: {
width: 6.5,
object: amountObj,
},
},
currency: {
priority: 2,
order: 5,
normal: {
width: 5.8,
object: currencyObj,
},
},
premium: {
priority: 3,
order: 11,
normal: {
width: 6,
object: premiumObj,
},
},
2022-10-01 12:03:01 +00:00
paymentMethod: {
priority: 4,
2022-10-01 12:03:01 +00:00
order: 6,
normal: {
2022-10-01 12:03:01 +00:00
width: 12.85,
object: paymentObj,
},
small: {
2022-10-01 12:03:01 +00:00
width: 4.4,
object: paymentSmallObj,
},
},
2022-10-01 12:03:01 +00:00
robot: {
priority: 5,
2022-10-01 12:03:01 +00:00
order: 1,
normal: {
2022-10-01 12:03:01 +00:00
width: 17.14,
object: robotObj,
},
small: {
2022-10-01 12:03:01 +00:00
width: 4.1,
object: robotSmallObj,
},
},
price: {
priority: 6,
order: 10,
normal: {
width: 10,
object: priceObj,
},
},
expires_at: {
priority: 7,
order: 7,
normal: {
width: 5,
object: expiryObj,
},
},
escrow_duration: {
priority: 8,
order: 8,
normal: {
2022-10-01 12:03:01 +00:00
width: 4.8,
object: timerObj,
},
},
satoshisNow: {
priority: 9,
order: 9,
normal: {
width: 6,
object: satoshisObj,
},
},
type: {
priority: 10,
order: 2,
normal: {
width: 4.3,
object: typeObj,
},
},
id: {
priority: 11,
order: 12,
normal: {
width: 4.8,
object: idObj,
},
},
};
const filteredColumns = function (maxWidth: number) {
const useSmall = maxWidth < 70;
const selectedColumns: object[] = [];
let width: number = 0;
for (const [key, value] of Object.entries(columnSpecs)) {
const colWidth = useSmall && value.small ? value.small.width : value.normal.width;
const colObject = useSmall && value.small ? value.small.object : value.normal.object;
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
width = width + colWidth;
selectedColumns.push([colObject(colWidth, false), value.order]);
} else {
selectedColumns.push([colObject(colWidth, true), value.order]);
}
}
// sort columns by column.order value
selectedColumns.sort(function (first, second) {
return first[1] - second[1];
});
const columns = selectedColumns.map(function (item) {
return item[0];
});
return [columns, width * 0.875 + 0.15];
};
2022-09-29 12:49:01 +00:00
const [columns, width] = filteredColumns(fullscreen ? fullWidth : maxWidth);
2022-09-29 11:53:12 +00:00
const gridComponents = {
LoadingOverlay: LinearProgress,
2022-09-29 11:53:12 +00:00
NoResultsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
{t('Filter has no results')}
</Stack>
),
Footer: () => (
2022-09-29 12:49:01 +00:00
<Grid container alignItems='center' direction='row' justifyContent='space-between'>
2022-09-29 11:53:12 +00:00
<Grid item>
<Grid container alignItems='center' direction='row'>
<Grid item xs={6}>
<IconButton onClick={() => setFullscreen(!fullscreen)}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Grid>
<Grid item xs={6}>
<IconButton onClick={clickRefresh}>
<Refresh />
</IconButton>
</Grid>
</Grid>
2022-09-29 11:53:12 +00:00
</Grid>
2022-09-29 11:53:12 +00:00
<Grid item>
2022-09-29 12:49:01 +00:00
<GridPagination />
2022-09-29 11:53:12 +00:00
</Grid>
</Grid>
),
2022-09-29 12:49:01 +00:00
};
2022-09-29 11:53:12 +00:00
2022-09-29 12:49:01 +00:00
if (!fullscreen) {
return (
2022-09-29 11:53:12 +00:00
<Paper style={{ width: `${width}em`, height: `${height}em`, overflow: 'auto' }}>
<DataGrid
2022-09-29 12:49:01 +00:00
localeText={localeText}
rows={orders.filter(
(order) =>
(order.type == type || type == null) && (order.currency == currency || currency == 0),
)}
loading={loading || refreshing}
2022-09-29 12:49:01 +00:00
columns={columns}
components={gridComponents}
pageSize={loading ? 0 : pageSize}
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
onRowClick={(params) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
/>
2022-09-29 11:53:12 +00:00
</Paper>
2022-09-29 12:49:01 +00:00
);
2022-09-29 11:53:12 +00:00
} else {
2022-09-29 12:49:01 +00:00
return (
<Dialog open={fullscreen} fullScreen={true}>
<Paper style={{ width: '100%', height: '100%', overflow: 'auto' }}>
<DataGrid
localeText={localeText}
rows={orders.filter(
(order) =>
(order.type == type || type == null) &&
(order.currency == currency || currency == 0),
)}
loading={loading || refreshing}
2022-09-29 12:49:01 +00:00
columns={columns}
components={gridComponents}
pageSize={loading ? 0 : pageSize}
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
onRowClick={(params) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
/>
</Paper>
</Dialog>
);
2022-09-29 11:53:12 +00:00
}
};
export default BookTable;