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 BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
|
||||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
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 = `${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
|
// errors
|
||||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
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,
|
BLOCK_API,
|
||||||
COUNTRY_DATA_API,
|
COUNTRY_DATA_API,
|
||||||
GATEWAYS_API,
|
GATEWAYS_API,
|
||||||
|
UPTIME_STORY_API_GATEWAY,
|
||||||
MIXNODE_API,
|
MIXNODE_API,
|
||||||
MIXNODE_PING,
|
MIXNODE_PING,
|
||||||
MIXNODES_API,
|
MIXNODES_API,
|
||||||
@@ -15,6 +16,8 @@ import {
|
|||||||
DelegationsResponse,
|
DelegationsResponse,
|
||||||
UniqDelegationsResponse,
|
UniqDelegationsResponse,
|
||||||
GatewayResponse,
|
GatewayResponse,
|
||||||
|
GatewayReportResponse,
|
||||||
|
UptimeStoryResponse,
|
||||||
MixNodeDescriptionResponse,
|
MixNodeDescriptionResponse,
|
||||||
MixNodeResponse,
|
MixNodeResponse,
|
||||||
MixNodeResponseItem,
|
MixNodeResponseItem,
|
||||||
@@ -23,7 +26,6 @@ import {
|
|||||||
StatsResponse,
|
StatsResponse,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
SummaryOverviewResponse,
|
SummaryOverviewResponse,
|
||||||
UptimeStoryResponse,
|
|
||||||
ValidatorsResponse,
|
ValidatorsResponse,
|
||||||
} from '../typeDefs/explorer-api';
|
} from '../typeDefs/explorer-api';
|
||||||
|
|
||||||
@@ -92,6 +94,12 @@ export class Api {
|
|||||||
return res.json();
|
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> => {
|
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||||
const res = await fetch(VALIDATORS_API);
|
const res = await fetch(VALIDATORS_API);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
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 { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { cellStyles } from './Universal-DataGrid';
|
import { cellStyles } from './Universal-DataGrid';
|
||||||
import { currencyToString } from '../utils/currency';
|
import { currencyToString } from '../utils/currency';
|
||||||
|
import { GatewayEnrichedRowType } from './Gateways';
|
||||||
import { MixnodeRowType } from './MixNodes';
|
import { MixnodeRowType } from './MixNodes';
|
||||||
|
|
||||||
export type ColumnsType = {
|
export type ColumnsType = {
|
||||||
@@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) {
|
|||||||
export const DetailTable: React.FC<{
|
export const DetailTable: React.FC<{
|
||||||
tableName: string;
|
tableName: string;
|
||||||
columnsData: ColumnsType[];
|
columnsData: ColumnsType[];
|
||||||
rows: MixnodeRowType[];
|
rows: MixnodeRowType[] | GatewayEnrichedRowType[];
|
||||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => (
|
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
|
||||||
<TableContainer component={Paper}>
|
const theme = useTheme();
|
||||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
return (
|
||||||
<TableHead>
|
<TableContainer component={Paper}>
|
||||||
<TableRow>
|
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||||
{columnsData?.map(({ field, title, flex }) => (
|
<TableHead>
|
||||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
<TableRow>
|
||||||
{title}
|
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||||
</TableCell>
|
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||||
))}
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
</TableRow>
|
{tooltipInfo && (
|
||||||
</TableHead>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<TableBody>
|
<Tooltip
|
||||||
{rows.map((eachRow) => (
|
title={tooltipInfo}
|
||||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
id={field}
|
||||||
{columnsData?.map((_, index) => (
|
placement="top-start"
|
||||||
<TableCell
|
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||||
key={_.title}
|
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||||
component="th"
|
maxWidth={230}
|
||||||
scope="row"
|
arrow
|
||||||
variant="body"
|
/>
|
||||||
sx={{
|
</Box>
|
||||||
...cellStyles,
|
)}
|
||||||
padding: 2,
|
{title}
|
||||||
width: 200,
|
</Box>
|
||||||
fontSize: 14,
|
|
||||||
}}
|
|
||||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
|
||||||
>
|
|
||||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{rows.map((eachRow) => (
|
||||||
</TableContainer>
|
<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 = {
|
export type GatewayRowType = {
|
||||||
id: string;
|
id: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
identity_key: string;
|
identityKey: string;
|
||||||
bond: number;
|
bond: number;
|
||||||
host: string;
|
host: string;
|
||||||
location: string;
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||||
|
routingScore: string;
|
||||||
|
avgUptime: string;
|
||||||
|
clientsPort: number;
|
||||||
|
mixPort: number;
|
||||||
|
};
|
||||||
|
|
||||||
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
||||||
return !arrayOfGateways
|
return !arrayOfGateways
|
||||||
? []
|
? []
|
||||||
: arrayOfGateways.map((gw) => ({
|
: arrayOfGateways.map((gw) => ({
|
||||||
id: gw.owner,
|
id: gw.owner,
|
||||||
owner: gw.owner,
|
owner: gw.owner,
|
||||||
identity_key: gw.gateway.identity_key || '',
|
identityKey: gw.gateway.identity_key || '',
|
||||||
location: gw?.gateway?.location || '',
|
location: gw?.gateway?.location || '',
|
||||||
bond: gw.pledge_amount.amount || 0,
|
bond: gw.pledge_amount.amount || 0,
|
||||||
host: gw.gateway.host || '',
|
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 * as React from 'react';
|
||||||
import { ApiState } from '../typeDefs/explorer-api';
|
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>(
|
export const useApiState = <T>(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
id: string,
|
||||||
fn: Function,
|
fn: (argId: string) => Promise<T>,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
): [ApiState<T>, WrappedApiFn<T>] => {
|
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
|
||||||
|
// stores the state
|
||||||
const [value, setValue] = React.useState<ApiState<T>>();
|
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 {
|
try {
|
||||||
|
// keep previous state and set to loading
|
||||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
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,
|
isLoading: false,
|
||||||
data,
|
data,
|
||||||
});
|
};
|
||||||
return data;
|
setValue(newValue);
|
||||||
|
return newValue;
|
||||||
} catch (error) {
|
} 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),
|
error: error instanceof Error ? error : new Error(errorMessage),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
};
|
||||||
return undefined;
|
setValue(newValue);
|
||||||
|
return newValue;
|
||||||
}
|
}
|
||||||
}, [setValue, fn]);
|
}, [setValue, fn, id, errorMessage]);
|
||||||
return [value || { isLoading: true }, wrappedFn];
|
return [value, wrappedFetchFn, clearValueFn];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchGateways = async () => {
|
const fetchGateways = async () => {
|
||||||
|
setGateways((d) => ({ ...d, isLoading: true }));
|
||||||
try {
|
try {
|
||||||
const data = await Api.fetchGateways();
|
const data = await Api.fetchGateways();
|
||||||
setGateways({ data, isLoading: false });
|
setGateways({ data, isLoading: false });
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
UptimeStoryResponse,
|
UptimeStoryResponse,
|
||||||
} from '../typeDefs/explorer-api';
|
} from '../typeDefs/explorer-api';
|
||||||
import { Api } from '../api';
|
import { Api } from '../api';
|
||||||
|
import { useApiState } from './hooks';
|
||||||
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
|
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
|||||||
|
|
||||||
return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>;
|
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 * 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 { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||||
import { SelectChangeEvent } from '@mui/material/Select';
|
import { SelectChangeEvent } from '@mui/material/Select';
|
||||||
import { useMainContext } from '../../context/main';
|
import { useMainContext } from '../../context/main';
|
||||||
@@ -43,29 +44,20 @@ export const PageGateways: React.FC = () => {
|
|||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: 'owner',
|
field: 'identityKey',
|
||||||
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',
|
|
||||||
headerName: 'Identity Key',
|
headerName: 'Identity Key',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 380,
|
width: 380,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<Typography sx={cellStyles} data-testid="identity-key">
|
<MuiLink
|
||||||
|
sx={{ ...cellStyles }}
|
||||||
|
component={RRDLink}
|
||||||
|
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||||
|
>
|
||||||
{params.value}
|
{params.value}
|
||||||
</Typography>
|
</MuiLink>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,6 +101,19 @@ export const PageGateways: React.FC = () => {
|
|||||||
</Button>
|
</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>) => {
|
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||||
@@ -133,7 +138,12 @@ export const PageGateways: React.FC = () => {
|
|||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
/>
|
/>
|
||||||
<UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} />
|
<UniversalDataGrid
|
||||||
|
pagination
|
||||||
|
rows={gatewayToGridRow(filteredGateways)}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom';
|
import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom';
|
||||||
import { BIG_DIPPER } from '../api/constants';
|
import { BIG_DIPPER } from '../api/constants';
|
||||||
import { PageGateways } from '../pages/Gateways';
|
import { PageGateways } from '../pages/Gateways';
|
||||||
|
import { PageGatewayDetail } from '../pages/GatewayDetail';
|
||||||
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
||||||
import { PageMixnodes } from '../pages/Mixnodes';
|
import { PageMixnodes } from '../pages/Mixnodes';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => (
|
|||||||
<Route path="mixnodes" element={<PageMixnodes />} />
|
<Route path="mixnodes" element={<PageMixnodes />} />
|
||||||
<Route path="mixnode/:id" element={<PageMixnodeDetail />} />
|
<Route path="mixnode/:id" element={<PageMixnodeDetail />} />
|
||||||
<Route path="gateways" element={<PageGateways />} />
|
<Route path="gateways" element={<PageGateways />} />
|
||||||
|
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
||||||
<Route path="validators" element={<ValidatorRoute />} />
|
<Route path="validators" element={<ValidatorRoute />} />
|
||||||
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
||||||
</ReactRouterRoutes>
|
</ReactRouterRoutes>
|
||||||
|
|||||||
@@ -126,12 +126,9 @@ export type GatewayResponse = GatewayResponseItem[];
|
|||||||
export interface GatewayReportResponse {
|
export interface GatewayReportResponse {
|
||||||
identity: string;
|
identity: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
most_recent_ipv4: boolean;
|
most_recent: number;
|
||||||
most_recent_ipv6: boolean;
|
last_hour: number;
|
||||||
last_hour_ipv4: number;
|
last_day: number;
|
||||||
last_hour_ipv6: number;
|
|
||||||
last_day_ipv4: number;
|
|
||||||
last_day_ipv6: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GatewayHistoryResponse = StatsResponse;
|
export type GatewayHistoryResponse = StatsResponse;
|
||||||
|
|||||||
Reference in New Issue
Block a user