Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9978b81e | |||
| 7cdfdf8437 | |||
| fca32424b4 | |||
| 7ad9c15c9e | |||
| 54716c7c09 | |||
| 2a0b1a3734 | |||
| 7f35b3660b | |||
| 8470288079 | |||
| d5a65841af | |||
| 76144928a0 | |||
| d6ffdb86ce | |||
| 74f9205be1 |
@@ -14,6 +14,7 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
GATEWAYS_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
@@ -23,7 +26,6 @@ import {
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
UptimeStoryResponse,
|
||||
ValidatorsResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
@@ -92,6 +94,12 @@ export class Api {
|
||||
return res.json();
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { Box } from '@mui/system';
|
||||
import { cellStyles } from './Universal-DataGrid';
|
||||
import { currencyToString } from '../utils/currency';
|
||||
import { GatewayEnrichedRowType } from './Gateways';
|
||||
import { MixnodeRowType } from './MixNodes';
|
||||
|
||||
export type ColumnsType = {
|
||||
@@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) {
|
||||
export const DetailTable: React.FC<{
|
||||
tableName: string;
|
||||
columnsData: ColumnsType[];
|
||||
rows: MixnodeRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
{title}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((_, index) => (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
rows: MixnodeRowType[] | GatewayEnrichedRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((data, index) => (
|
||||
<TableCell
|
||||
key={data.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${data.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
import { GatewayResponse } from '../typeDefs/explorer-api';
|
||||
import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api';
|
||||
|
||||
export type GatewayRowType = {
|
||||
id: string;
|
||||
owner: string;
|
||||
identity_key: string;
|
||||
identityKey: string;
|
||||
bond: number;
|
||||
host: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||
routingScore: string;
|
||||
avgUptime: string;
|
||||
clientsPort: number;
|
||||
mixPort: number;
|
||||
};
|
||||
|
||||
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
||||
return !arrayOfGateways
|
||||
? []
|
||||
: arrayOfGateways.map((gw) => ({
|
||||
id: gw.owner,
|
||||
owner: gw.owner,
|
||||
identity_key: gw.gateway.identity_key || '',
|
||||
identityKey: gw.gateway.identity_key || '',
|
||||
location: gw?.gateway?.location || '',
|
||||
bond: gw.pledge_amount.amount || 0,
|
||||
host: gw.gateway.host || '',
|
||||
}));
|
||||
}
|
||||
|
||||
export function gatewayEnrichedToGridRow(
|
||||
gateway: GatewayResponseItem,
|
||||
report: GatewayReportResponse,
|
||||
): GatewayEnrichedRowType {
|
||||
return {
|
||||
id: gateway.owner,
|
||||
owner: gateway.owner,
|
||||
identityKey: gateway.gateway.identity_key || '',
|
||||
location: gateway?.gateway?.location || '',
|
||||
bond: gateway.pledge_amount.amount || 0,
|
||||
host: gateway.gateway.host || '',
|
||||
clientsPort: gateway.gateway.clients_port || 0,
|
||||
mixPort: gateway.gateway.mix_port || 0,
|
||||
routingScore: `${report.most_recent}%`,
|
||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
|
||||
/**
|
||||
* This context provides the state for a single gateway by identity key.
|
||||
*/
|
||||
|
||||
interface GatewayState {
|
||||
uptimeReport?: ApiState<GatewayReportResponse>;
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>;
|
||||
}
|
||||
|
||||
export const GatewayContext = React.createContext<GatewayState>({});
|
||||
|
||||
export const useGatewayContext = (): React.ContextType<typeof GatewayContext> =>
|
||||
React.useContext<GatewayState>(GatewayContext);
|
||||
|
||||
/**
|
||||
* Provides a state context for a gateway by identity
|
||||
* @param gatewayIdentityKey The identity key of the gateway
|
||||
*/
|
||||
export const GatewayContextProvider = ({
|
||||
gatewayIdentityKey,
|
||||
children,
|
||||
}: {
|
||||
gatewayIdentityKey: string;
|
||||
children: JSX.Element;
|
||||
}) => {
|
||||
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayReportById,
|
||||
'Failed to fetch gateway uptime report by id',
|
||||
);
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayUptimeStoryById,
|
||||
'Failed to fetch gateway uptime history',
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearUptimeReportById();
|
||||
clearUptimeHistory();
|
||||
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]);
|
||||
}, [gatewayIdentityKey]);
|
||||
|
||||
const state = React.useMemo<GatewayState>(
|
||||
() => ({
|
||||
uptimeReport,
|
||||
uptimeStory,
|
||||
}),
|
||||
[uptimeReport, uptimeStory],
|
||||
);
|
||||
|
||||
return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>;
|
||||
};
|
||||
@@ -1,30 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState } from '../typeDefs/explorer-api';
|
||||
|
||||
type WrappedApiFn<T> = () => Promise<ApiState<T>>;
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
export const useApiState = <T>(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
fn: Function,
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T>, WrappedApiFn<T>] => {
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T>>();
|
||||
const wrappedFn = React.useCallback(async () => {
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
setValue({ isLoading: true });
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
const data = await fn();
|
||||
setValue({
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
setValue({
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFn];
|
||||
}, [setValue, fn, id, errorMessage]);
|
||||
return [value, wrappedFetchFn, clearValueFn];
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
};
|
||||
|
||||
const fetchGateways = async () => {
|
||||
setGateways((d) => ({ ...d, isLoading: true }));
|
||||
try {
|
||||
const data = await Api.fetchGateways();
|
||||
setGateways({ data, isLoading: false });
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
|
||||
|
||||
/**
|
||||
@@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
|
||||
return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
function useApiState<T>(
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T> | undefined>();
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFetchFn, clearValueFn];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { GatewayResponseItem } from '../../typeDefs/explorer-api';
|
||||
import { ColumnsType, DetailTable } from '../../components/DetailTable';
|
||||
import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways';
|
||||
import { ComponentError } from '../../components/ComponentError';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
|
||||
import { UptimeChart } from '../../components/UptimeChart';
|
||||
import { GatewayContextProvider, useGatewayContext } from '../../context/gateway';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { Title } from '../../components/Title';
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'identityKey',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'routingScore',
|
||||
title: 'Routing Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Is the average routing score in the last 24 hours',
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'IP',
|
||||
headerAlign: 'left',
|
||||
width: 99,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows gateway details
|
||||
*/
|
||||
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => {
|
||||
const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>();
|
||||
const [status, setStatus] = React.useState<number[] | undefined>();
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uptimeReport?.data && selectedGateway) {
|
||||
setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data));
|
||||
}
|
||||
}, [uptimeReport, selectedGateway]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enrichGateway) {
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
|
||||
}
|
||||
}, [enrichGateway]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Title text="Gateway Detail" />
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<DetailTable
|
||||
columnsData={columns}
|
||||
tableName="Gateway detail table"
|
||||
rows={enrichGateway ? [enrichGateway] : []}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
{status && (
|
||||
<ContentCard title="Gateway Status">
|
||||
<TwoColSmallTable
|
||||
loading={false}
|
||||
keys={['Mix port', 'Client WS API Port']}
|
||||
values={status.map((each) => each)}
|
||||
icons={status.map((elem) => !!elem)}
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
{uptimeStory && (
|
||||
<ContentCard title="Routing Score">
|
||||
{uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />}
|
||||
<UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} />
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard: React.FC = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
|
||||
}
|
||||
}, [gateways, id]);
|
||||
|
||||
if (gateways?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (gateways?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(gateways?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
if (gateways && !gateways.isLoading && !gateways.data) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Gateway not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
export const PageGatewayDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
if (!id) {
|
||||
return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GatewayContextProvider gatewayIdentityKey={id}>
|
||||
<PageGatewayDetailGuard />
|
||||
</GatewayContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Card, Grid, Typography } from '@mui/material';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { Button, Card, Grid, Typography, Link as MuiLink } from '@mui/material';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { useMainContext } from '../../context/main';
|
||||
@@ -43,29 +44,20 @@ export const PageGateways: React.FC = () => {
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="owner">
|
||||
{params.value}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
field: 'identityKey',
|
||||
headerName: 'Identity Key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="identity-key">
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||
>
|
||||
{params.value}
|
||||
</Typography>
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -109,6 +101,19 @@ export const PageGateways: React.FC = () => {
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="owner">
|
||||
{params.value}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
@@ -133,7 +138,12 @@ export const PageGateways: React.FC = () => {
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} />
|
||||
<UniversalDataGrid
|
||||
pagination
|
||||
rows={gatewayToGridRow(filteredGateways)}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom';
|
||||
import { BIG_DIPPER } from '../api/constants';
|
||||
import { PageGateways } from '../pages/Gateways';
|
||||
import { PageGatewayDetail } from '../pages/GatewayDetail';
|
||||
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
||||
import { PageMixnodes } from '../pages/Mixnodes';
|
||||
|
||||
@@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => (
|
||||
<Route path="mixnodes" element={<PageMixnodes />} />
|
||||
<Route path="mixnode/:id" element={<PageMixnodeDetail />} />
|
||||
<Route path="gateways" element={<PageGateways />} />
|
||||
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
||||
<Route path="validators" element={<ValidatorRoute />} />
|
||||
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
||||
</ReactRouterRoutes>
|
||||
|
||||
@@ -126,12 +126,9 @@ export type GatewayResponse = GatewayResponseItem[];
|
||||
export interface GatewayReportResponse {
|
||||
identity: string;
|
||||
owner: string;
|
||||
most_recent_ipv4: boolean;
|
||||
most_recent_ipv6: boolean;
|
||||
last_hour_ipv4: number;
|
||||
last_hour_ipv6: number;
|
||||
last_day_ipv4: number;
|
||||
last_day_ipv6: number;
|
||||
most_recent: number;
|
||||
last_hour: number;
|
||||
last_day: number;
|
||||
}
|
||||
|
||||
export type GatewayHistoryResponse = StatsResponse;
|
||||
|
||||
Reference in New Issue
Block a user