Network Explorer: Mixnode filters (#1460)
* add filters UI * use filter schema * filter mixnode based on selected filters * only show filters on the mixnode page * use base api to get all mixnodes to avoid setting mixnodes in state * prevent additional request when status changes * create isMobile hook
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Nav } from './components/Nav';
|
||||
import { MobileNav } from './components/MobileNav';
|
||||
import { Routes } from './routes/index';
|
||||
import { useIsMobile } from './hooks/useIsMobile';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Tune } from '@mui/icons-material';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Slider,
|
||||
Typography,
|
||||
Box,
|
||||
Snackbar,
|
||||
Slide,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { MixnodeStatusWithAll, toMixnodeStatus } from '../../typeDefs/explorer-api';
|
||||
import { EnumFilterKey, TFilterItem, TFilters } from '../../typeDefs/filters';
|
||||
import { formatOnSave, generateFilterSchema } from './filterSchema';
|
||||
import { Api } from '../../api';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
|
||||
const FilterItem = ({
|
||||
label,
|
||||
id,
|
||||
value,
|
||||
marks,
|
||||
scale,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
}: TFilterItem & {
|
||||
onChange: (id: EnumFilterKey, newValue: number[]) => void;
|
||||
}) => (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography gutterBottom>{label}</Typography>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])}
|
||||
valueLabelDisplay="off"
|
||||
marks={marks}
|
||||
step={null}
|
||||
scale={scale}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Filters = () => {
|
||||
const { filterMixnodes, fetchMixnodes } = useMainContext();
|
||||
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [isFiltered, setIsFiltered] = useState(false);
|
||||
const [filters, setFilters] = React.useState<TFilters>();
|
||||
|
||||
const baseFilters = useRef<TFilters>();
|
||||
const prevFilters = useRef<TFilters>();
|
||||
|
||||
const handleToggleShowFilters = () => setShowFilters(!showFilters);
|
||||
|
||||
const initialiseFilters = useCallback(async () => {
|
||||
let upperSaturationValue;
|
||||
const allMixnodes = await Api.fetchMixnodes();
|
||||
if (allMixnodes) {
|
||||
upperSaturationValue = Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1);
|
||||
const initFilters = generateFilterSchema(upperSaturationValue);
|
||||
baseFilters.current = initFilters;
|
||||
prevFilters.current = initFilters;
|
||||
setFilters(initFilters);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (id: EnumFilterKey, newValue: number[]) => {
|
||||
setFilters((ftrs) => {
|
||||
if (ftrs) return { ...ftrs, [id]: { ...ftrs[id], value: newValue } };
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnSave = async () => {
|
||||
setShowFilters(false);
|
||||
await filterMixnodes(formatOnSave(filters!), status);
|
||||
setIsFiltered(true);
|
||||
prevFilters.current = filters;
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
setShowFilters(false);
|
||||
setFilters(prevFilters.current);
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters(baseFilters.current);
|
||||
setIsFiltered(false);
|
||||
prevFilters.current = baseFilters.current;
|
||||
};
|
||||
|
||||
const onClearFilters = async () => {
|
||||
await fetchMixnodes(toMixnodeStatus(status));
|
||||
resetFilters();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initialiseFilters();
|
||||
}, [initialiseFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [status]);
|
||||
|
||||
if (!filters) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar
|
||||
open={isFiltered}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
message="Filters applied"
|
||||
TransitionComponent={Slide}
|
||||
transitionDuration={250}
|
||||
>
|
||||
<Alert
|
||||
severity="info"
|
||||
variant={isMobile ? 'standard' : 'outlined'}
|
||||
action={
|
||||
<Button size="small" onClick={onClearFilters}>
|
||||
Clear
|
||||
</Button>
|
||||
}
|
||||
sx={{ width: 300 }}
|
||||
>
|
||||
Filters applied
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<IconButton size="large" onClick={handleToggleShowFilters}>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
<Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Mixnode filters</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{Object.values(filters).map((v) => (
|
||||
<FilterItem {...v} key={v.id} onChange={handleOnChange} />
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button size="large" onClick={handleOnCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" size="large" onClick={handleOnSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { EnumFilterKey, TFilters } from '../../typeDefs/filters';
|
||||
|
||||
export const generateFilterSchema = (upperSaturationValue?: number) => ({
|
||||
profitMargin: {
|
||||
label: 'Profit margin (%)',
|
||||
id: EnumFilterKey.profitMargin,
|
||||
value: [0, 100],
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
},
|
||||
stakeSaturation: {
|
||||
label: 'Stake saturation (%)',
|
||||
id: EnumFilterKey.stakeSaturation,
|
||||
value: [0, upperSaturationValue || 100],
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
|
||||
{
|
||||
label: '10',
|
||||
value: 10,
|
||||
},
|
||||
|
||||
{
|
||||
label: '50',
|
||||
value: 50,
|
||||
},
|
||||
{ label: '90', value: 90 },
|
||||
|
||||
{
|
||||
label: upperSaturationValue ? `${upperSaturationValue}` : '100',
|
||||
value: upperSaturationValue || 100,
|
||||
},
|
||||
],
|
||||
max: upperSaturationValue,
|
||||
},
|
||||
stake: {
|
||||
label: 'Stake (NYM)',
|
||||
id: EnumFilterKey.stake,
|
||||
value: [20, 90],
|
||||
min: 20,
|
||||
max: 90,
|
||||
marks: [
|
||||
{
|
||||
value: 0,
|
||||
label: '1',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: '10',
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
label: '100',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '1k',
|
||||
},
|
||||
{
|
||||
value: 40,
|
||||
label: '10k',
|
||||
},
|
||||
{
|
||||
value: 50,
|
||||
label: '100k',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '1M',
|
||||
},
|
||||
{
|
||||
value: 70,
|
||||
label: '10M',
|
||||
},
|
||||
{
|
||||
value: 80,
|
||||
label: '100M',
|
||||
},
|
||||
{
|
||||
value: 90,
|
||||
label: '1B',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const formatStakeValuesToMinorDenom = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = 10 ** (value_1 / 10) * 1_000_000;
|
||||
const upperValue = 10 ** (value_2 / 10) * 1_000_000;
|
||||
|
||||
return [lowerValue, upperValue];
|
||||
};
|
||||
|
||||
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = value_1 / 100;
|
||||
const upperValue = value_2 / 100;
|
||||
|
||||
return [lowerValue, upperValue];
|
||||
};
|
||||
|
||||
export const formatOnSave = (filters: TFilters) => ({
|
||||
stake: formatStakeValuesToMinorDenom(filters.stake.value),
|
||||
profitMargin: filters.profitMargin.value,
|
||||
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Socials } from './Socials';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -39,7 +38,7 @@ export const Footer: React.FC = () => {
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
textAlign: isMobile ? 'center' : 'end',
|
||||
color: theme.palette.nym.muted.onDarkBg,
|
||||
color: 'nym.muted.onDarkBg',
|
||||
}}
|
||||
>
|
||||
© {new Date().getFullYear()} Nym Technologies SA, all rights reserved
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, Box, CircularProgress, useMediaQuery, Typography } from '@mui/material';
|
||||
import { Alert, Box, CircularProgress, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -11,6 +11,7 @@ import Paper from '@mui/material/Paper';
|
||||
import { ExpandMore } from '@mui/icons-material';
|
||||
import { currencyToString } from '../../utils/currency';
|
||||
import { useMixnodeContext } from '../../context/mixnode';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
|
||||
export const BondBreakdownTable: React.FC = () => {
|
||||
const { mixNode, delegations, uniqDelegations } = useMixnodeContext();
|
||||
@@ -23,7 +24,7 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
hasLoaded: false,
|
||||
});
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mixNode?.data) {
|
||||
@@ -83,7 +84,7 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
|
||||
<TableBody>
|
||||
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
|
||||
<TableRow sx={isMobile ? { minWidth: '70vw' } : null}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
@@ -187,7 +188,7 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
<TableBody>
|
||||
{uniqDelegations?.data?.map(({ owner, amount: { amount, denom } }) => (
|
||||
<TableRow key={owner}>
|
||||
<TableCell sx={matches ? { width: 190 } : null} align="left">
|
||||
<TableCell sx={isMobile ? { width: 190 } : null} align="left">
|
||||
{owner}
|
||||
</TableCell>
|
||||
<TableCell align="left">{currencyToString(amount.toString(), denom)}</TableCell>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Box, Button, Grid, Typography, useMediaQuery } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Grid, Typography, useTheme } from '@mui/material';
|
||||
import Identicon from 'react-identicons';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { MixnodeRowType } from '.';
|
||||
import { getMixNodeStatusText, MixNodeStatus } from './Status';
|
||||
import { MixNodeDescriptionResponse } from '../../typeDefs/explorer-api';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
|
||||
interface MixNodeDetailProps {
|
||||
mixNodeRow: MixnodeRowType;
|
||||
@@ -14,7 +14,7 @@ interface MixNodeDetailProps {
|
||||
export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({ mixNodeRow, mixnodeDescription }) => {
|
||||
const theme = useTheme();
|
||||
const palette = [theme.palette.text.primary];
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMobile = useIsMobile();
|
||||
const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]);
|
||||
return (
|
||||
<Grid container>
|
||||
@@ -64,7 +64,7 @@ export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({ mixNodeRow,
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} display="flex" justifyContent="end" mt={matches ? 5 : undefined}>
|
||||
<Grid item xs={12} sm={6} display="flex" justifyContent="end" mt={isMobile ? 5 : undefined}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography fontWeight="600" alignSelf="self-end">
|
||||
Node status:
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
@@ -17,6 +7,7 @@ import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
import { cellStyles } from '../../Universal-DataGrid';
|
||||
import { UniversalTableProps } from '../../DetailTable';
|
||||
import { useIsMobile } from '../../../hooks/useIsMobile';
|
||||
|
||||
const threshold = 100;
|
||||
|
||||
@@ -44,8 +35,8 @@ const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
||||
return theme.palette.nym.wallet.fee;
|
||||
};
|
||||
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string) => {
|
||||
const isTablet = useIsMobile('lg');
|
||||
if (value.progressBarValue) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
|
||||
@@ -130,7 +121,7 @@ export const DelegatorsInfoTable: React.FC<UniversalTableProps<EconomicsInfoRowW
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(value, columnsData[index].field, theme)}
|
||||
{formatCellValues(value, columnsData[index].field)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { MenuItem, useMediaQuery } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { MenuItem } from '@mui/material';
|
||||
import Select from '@mui/material/Select';
|
||||
import { SelectInputProps } from '@mui/material/Select/SelectInput';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { SxProps } from '@mui/system';
|
||||
import { MixNodeStatus } from './Status';
|
||||
import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
|
||||
// TODO: replace with i18n
|
||||
const ALL_NODES = 'All nodes';
|
||||
@@ -17,8 +17,7 @@ interface MixNodeStatusDropdownProps {
|
||||
}
|
||||
|
||||
export const MixNodeStatusDropdown: React.FC<MixNodeStatusDropdownProps> = ({ status, onSelectionChanged, sx }) => {
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMobile = useIsMobile();
|
||||
const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(status || MixnodeStatusWithAll.all);
|
||||
const onChange: SelectInputProps<MixnodeStatusWithAll>['onChange'] = React.useCallback(
|
||||
({ target: { value } }) => {
|
||||
@@ -47,7 +46,7 @@ export const MixNodeStatusDropdown: React.FC<MixNodeStatusDropdownProps> = ({ st
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: matches ? 'auto' : 200,
|
||||
width: isMobile ? 'auto' : 200,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { Box, useMediaQuery, TextField, MenuItem } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, TextField, MenuItem } from '@mui/material';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { Filters } from './Filters/Filters';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
type TableToolBarProps = {
|
||||
onChangeSearch: (arg: string) => void;
|
||||
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
||||
pageSize: string;
|
||||
searchTerm: string;
|
||||
withFilters?: boolean;
|
||||
childrenBefore?: React.ReactNode;
|
||||
childrenAfter?: React.ReactNode;
|
||||
};
|
||||
@@ -19,16 +21,16 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
|
||||
pageSize,
|
||||
childrenBefore,
|
||||
childrenAfter,
|
||||
withFilters,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isMobile = useIsMobile();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: matches ? 'column-reverse' : 'row',
|
||||
flexDirection: isMobile ? 'column-reverse' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
@@ -40,7 +42,7 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
|
||||
value={pageSize}
|
||||
onChange={onChangePageSize}
|
||||
sx={{
|
||||
width: matches ? 100 : 200,
|
||||
width: isMobile ? 100 : 200,
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10} data-testid="ten">
|
||||
@@ -57,17 +59,18 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'middle' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TextField
|
||||
sx={{
|
||||
width: matches ? '100%' : 350,
|
||||
marginBottom: matches ? 2 : 0,
|
||||
width: isMobile ? '100%' : 350,
|
||||
marginBottom: isMobile ? 2 : 0,
|
||||
}}
|
||||
value={searchTerm}
|
||||
data-testid="search-box"
|
||||
placeholder="search"
|
||||
onChange={(event) => onChangeSearch(event.target.value)}
|
||||
/>
|
||||
{withFilters && <Filters />}
|
||||
{childrenAfter}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SummaryOverviewResponse,
|
||||
ValidatorsResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
import { EnumFilterKey } from '../typeDefs/filters';
|
||||
import { Api } from '../api';
|
||||
import { NavOptionType, originalNavOptions } from './nav';
|
||||
|
||||
@@ -26,8 +27,8 @@ interface StateData {
|
||||
}
|
||||
|
||||
interface StateApi {
|
||||
fetchMixnodes: (status?: MixnodeStatus) => void;
|
||||
filterMixnodes: (mixnodes: MixNodeResponse) => void;
|
||||
fetchMixnodes: (status?: MixnodeStatus) => Promise<MixNodeResponse | undefined>;
|
||||
filterMixnodes: (filters: any, status: any) => void;
|
||||
toggleMode: () => void;
|
||||
updateNavState: (id: number) => void;
|
||||
}
|
||||
@@ -40,7 +41,7 @@ export const MainContext = React.createContext<State>({
|
||||
navState: originalNavOptions,
|
||||
toggleMode: () => undefined,
|
||||
filterMixnodes: () => null,
|
||||
fetchMixnodes: () => null,
|
||||
fetchMixnodes: () => Promise.resolve(undefined),
|
||||
});
|
||||
|
||||
export const useMainContext = (): React.ContextType<typeof MainContext> => React.useContext<State>(MainContext);
|
||||
@@ -78,9 +79,10 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
};
|
||||
|
||||
const fetchMixnodes = async (status?: MixnodeStatus) => {
|
||||
let data;
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }));
|
||||
try {
|
||||
const data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes();
|
||||
data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes();
|
||||
setMixnodes({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setMixnodes({
|
||||
@@ -88,10 +90,24 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
const filterMixnodes = (arr: MixNodeResponse) => {
|
||||
setMixnodes({ data: arr, isLoading: false });
|
||||
|
||||
const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => {
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }));
|
||||
const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes();
|
||||
const filtered = mxns?.filter(
|
||||
(m) =>
|
||||
m.mix_node.profit_margin_percent >= filters.profitMargin[0] &&
|
||||
m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
|
||||
m.stake_saturation >= filters.stakeSaturation[0] &&
|
||||
m.stake_saturation <= filters.stakeSaturation[1] &&
|
||||
+m.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] &&
|
||||
+m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1],
|
||||
);
|
||||
setMixnodes({ data: filtered, isLoading: false });
|
||||
};
|
||||
|
||||
const fetchGateways = async () => {
|
||||
try {
|
||||
const data = await Api.fetchGateways();
|
||||
@@ -144,6 +160,7 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
}));
|
||||
updateNav(updated);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]);
|
||||
}, []);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Breakpoint, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const useIsMobile = (queryInput: number | Breakpoint = 'md') => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(queryInput));
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
@@ -290,6 +290,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
withFilters
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
pagination
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Mark } from '@mui/base';
|
||||
|
||||
export enum EnumFilterKey {
|
||||
profitMargin = 'profitMargin',
|
||||
stakeSaturation = 'stakeSaturation',
|
||||
stake = 'stake',
|
||||
}
|
||||
|
||||
export type TFilterItem = {
|
||||
label: string;
|
||||
id: EnumFilterKey;
|
||||
value: number[];
|
||||
marks: Mark[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
scale?: (value: number) => number;
|
||||
};
|
||||
|
||||
export type TFilters = { [key in EnumFilterKey]: TFilterItem };
|
||||
Reference in New Issue
Block a user