mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-01-04 06:23:08 +00:00
Add peers page
This commit is contained in:
parent
6c96cbe295
commit
29ceaea8c5
50
src/components/Peers/PeerCountryCell.tsx
Normal file
50
src/components/Peers/PeerCountryCell.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Cell } from "@codex-storage/marketplace-ui-components";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PeerPin } from "./types";
|
||||
import { countriesCoordinates } from "./countries";
|
||||
|
||||
export type Props = {
|
||||
address: string;
|
||||
onPinAdd: (pin: PeerPin) => void;
|
||||
};
|
||||
|
||||
const getFlagEmoji = (countryCode: string) => {
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
export function PeerCountryCell({ address, onPinAdd }: Props) {
|
||||
const [country, setCountry] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const [ip] = address.split(":");
|
||||
|
||||
console.info(ip);
|
||||
|
||||
fetch("https://api.country.is/" + ip)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
setCountry(json.country);
|
||||
|
||||
const coordinate = countriesCoordinates.find(
|
||||
(c) => c.iso === json.country
|
||||
);
|
||||
|
||||
if (coordinate) {
|
||||
onPinAdd({
|
||||
lat: parseFloat(coordinate.lat),
|
||||
lng: parseFloat(coordinate.lng),
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<Cell>
|
||||
{getFlagEmoji(country)} {address}
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
1312
src/components/Peers/countries.ts
Normal file
1312
src/components/Peers/countries.ts
Normal file
File diff suppressed because it is too large
Load Diff
4
src/components/Peers/types.ts
Normal file
4
src/components/Peers/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type PeerPin = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
@ -17,6 +17,7 @@ import { Route as DashboardIndexImport } from './routes/dashboard/index'
|
||||
import { Route as DashboardSettingsImport } from './routes/dashboard/settings'
|
||||
import { Route as DashboardRequestsImport } from './routes/dashboard/requests'
|
||||
import { Route as DashboardPurchasesImport } from './routes/dashboard/purchases'
|
||||
import { Route as DashboardPeersImport } from './routes/dashboard/peers'
|
||||
import { Route as DashboardHelpImport } from './routes/dashboard/help'
|
||||
import { Route as DashboardFavoritesImport } from './routes/dashboard/favorites'
|
||||
import { Route as DashboardDisclaimerImport } from './routes/dashboard/disclaimer'
|
||||
@ -55,6 +56,11 @@ const DashboardPurchasesRoute = DashboardPurchasesImport.update({
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
|
||||
const DashboardPeersRoute = DashboardPeersImport.update({
|
||||
path: '/peers',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
|
||||
const DashboardHelpRoute = DashboardHelpImport.update({
|
||||
path: '/help',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
@ -133,6 +139,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DashboardHelpImport
|
||||
parentRoute: typeof DashboardImport
|
||||
}
|
||||
'/dashboard/peers': {
|
||||
id: '/dashboard/peers'
|
||||
path: '/peers'
|
||||
fullPath: '/dashboard/peers'
|
||||
preLoaderRoute: typeof DashboardPeersImport
|
||||
parentRoute: typeof DashboardImport
|
||||
}
|
||||
'/dashboard/purchases': {
|
||||
id: '/dashboard/purchases'
|
||||
path: '/purchases'
|
||||
@ -172,6 +185,7 @@ interface DashboardRouteChildren {
|
||||
DashboardDisclaimerRoute: typeof DashboardDisclaimerRoute
|
||||
DashboardFavoritesRoute: typeof DashboardFavoritesRoute
|
||||
DashboardHelpRoute: typeof DashboardHelpRoute
|
||||
DashboardPeersRoute: typeof DashboardPeersRoute
|
||||
DashboardPurchasesRoute: typeof DashboardPurchasesRoute
|
||||
DashboardRequestsRoute: typeof DashboardRequestsRoute
|
||||
DashboardSettingsRoute: typeof DashboardSettingsRoute
|
||||
@ -184,6 +198,7 @@ const DashboardRouteChildren: DashboardRouteChildren = {
|
||||
DashboardDisclaimerRoute: DashboardDisclaimerRoute,
|
||||
DashboardFavoritesRoute: DashboardFavoritesRoute,
|
||||
DashboardHelpRoute: DashboardHelpRoute,
|
||||
DashboardPeersRoute: DashboardPeersRoute,
|
||||
DashboardPurchasesRoute: DashboardPurchasesRoute,
|
||||
DashboardRequestsRoute: DashboardRequestsRoute,
|
||||
DashboardSettingsRoute: DashboardSettingsRoute,
|
||||
@ -202,6 +217,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
|
||||
'/dashboard/favorites': typeof DashboardFavoritesRoute
|
||||
'/dashboard/help': typeof DashboardHelpRoute
|
||||
'/dashboard/peers': typeof DashboardPeersRoute
|
||||
'/dashboard/purchases': typeof DashboardPurchasesRoute
|
||||
'/dashboard/requests': typeof DashboardRequestsRoute
|
||||
'/dashboard/settings': typeof DashboardSettingsRoute
|
||||
@ -215,6 +231,7 @@ export interface FileRoutesByTo {
|
||||
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
|
||||
'/dashboard/favorites': typeof DashboardFavoritesRoute
|
||||
'/dashboard/help': typeof DashboardHelpRoute
|
||||
'/dashboard/peers': typeof DashboardPeersRoute
|
||||
'/dashboard/purchases': typeof DashboardPurchasesRoute
|
||||
'/dashboard/requests': typeof DashboardRequestsRoute
|
||||
'/dashboard/settings': typeof DashboardSettingsRoute
|
||||
@ -230,6 +247,7 @@ export interface FileRoutesById {
|
||||
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
|
||||
'/dashboard/favorites': typeof DashboardFavoritesRoute
|
||||
'/dashboard/help': typeof DashboardHelpRoute
|
||||
'/dashboard/peers': typeof DashboardPeersRoute
|
||||
'/dashboard/purchases': typeof DashboardPurchasesRoute
|
||||
'/dashboard/requests': typeof DashboardRequestsRoute
|
||||
'/dashboard/settings': typeof DashboardSettingsRoute
|
||||
@ -246,6 +264,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/disclaimer'
|
||||
| '/dashboard/favorites'
|
||||
| '/dashboard/help'
|
||||
| '/dashboard/peers'
|
||||
| '/dashboard/purchases'
|
||||
| '/dashboard/requests'
|
||||
| '/dashboard/settings'
|
||||
@ -258,6 +277,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/disclaimer'
|
||||
| '/dashboard/favorites'
|
||||
| '/dashboard/help'
|
||||
| '/dashboard/peers'
|
||||
| '/dashboard/purchases'
|
||||
| '/dashboard/requests'
|
||||
| '/dashboard/settings'
|
||||
@ -271,6 +291,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/disclaimer'
|
||||
| '/dashboard/favorites'
|
||||
| '/dashboard/help'
|
||||
| '/dashboard/peers'
|
||||
| '/dashboard/purchases'
|
||||
| '/dashboard/requests'
|
||||
| '/dashboard/settings'
|
||||
@ -315,6 +336,7 @@ export const routeTree = rootRoute
|
||||
"/dashboard/disclaimer",
|
||||
"/dashboard/favorites",
|
||||
"/dashboard/help",
|
||||
"/dashboard/peers",
|
||||
"/dashboard/purchases",
|
||||
"/dashboard/requests",
|
||||
"/dashboard/settings",
|
||||
@ -341,6 +363,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "dashboard/help.tsx",
|
||||
"parent": "/dashboard"
|
||||
},
|
||||
"/dashboard/peers": {
|
||||
"filePath": "dashboard/peers.tsx",
|
||||
"parent": "/dashboard"
|
||||
},
|
||||
"/dashboard/purchases": {
|
||||
"filePath": "dashboard/purchases.tsx",
|
||||
"parent": "/dashboard"
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Settings,
|
||||
HelpCircle,
|
||||
TriangleAlert,
|
||||
Earth,
|
||||
} from "lucide-react";
|
||||
import { ICON_SIZE } from "../utils/constants";
|
||||
import { NodeIndicator } from "../components/NodeIndicator/NodeIndicator";
|
||||
@ -87,6 +88,15 @@ const Layout = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
<Link to="/dashboard/peers" {...p}>
|
||||
<Earth size={ICON_SIZE} />
|
||||
Peers
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
|
||||
10
src/routes/dashboard/peers.css
Normal file
10
src/routes/dashboard/peers.css
Normal file
@ -0,0 +1,10 @@
|
||||
.peers-map,
|
||||
.peers-table {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.peers-table {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
98
src/routes/dashboard/peers.tsx
Normal file
98
src/routes/dashboard/peers.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Cell, Row, Table } from "@codex-storage/marketplace-ui-components";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getMapJSON } from "dotted-map";
|
||||
import DottedMap from "dotted-map/without-countries";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { PeerCountryCell } from "../../components/Peers/PeerCountryCell";
|
||||
import { useCallback, useState } from "react";
|
||||
import { PeerPin } from "../../components/Peers/types";
|
||||
import "./peers.css";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
|
||||
// This function accepts the same arguments as DottedMap in the example above.
|
||||
const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
|
||||
|
||||
export const Route = createFileRoute("/dashboard/peers")({
|
||||
component: () => {
|
||||
const [pins, setPins] = useState<[PeerPin, number][]>([]);
|
||||
const { data } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.debug.info().then((s) => Promises.rejectOnError(s)),
|
||||
queryKey: ["debug"],
|
||||
|
||||
// No need to retry because if the connection to the node
|
||||
// is back again, all the queries will be invalidated.
|
||||
retry: false,
|
||||
|
||||
// The client node should be local, so display the cache value while
|
||||
// making a background request looks good.
|
||||
staleTime: 0,
|
||||
|
||||
// Refreshing when focus returns can be useful if a user comes back
|
||||
// to the UI after performing an operation in the terminal.
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
// Throw the error to the error boundary
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
const onPinAdd = useCallback((pin: PeerPin) => {
|
||||
setPins((val) => {
|
||||
const [, quantity = 0] =
|
||||
val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
|
||||
return [...val, [pin, quantity + 1]];
|
||||
});
|
||||
}, []);
|
||||
|
||||
// It’s safe to re-create the map at each render, because of the
|
||||
// pre-computation it’s super fast ⚡️
|
||||
const map = new DottedMap({ map: JSON.parse(mapJsonString) });
|
||||
|
||||
pins.map(([pin, quantity]) =>
|
||||
map.addPin({
|
||||
lat: pin.lat,
|
||||
lng: pin.lng,
|
||||
svgOptions: { color: "#d6ff79", radius: 0.4 * quantity },
|
||||
})
|
||||
);
|
||||
|
||||
const svgMap = map.getSVG({
|
||||
radius: 0.42,
|
||||
color: "#423B38",
|
||||
shape: "circle",
|
||||
backgroundColor: "#020300",
|
||||
});
|
||||
|
||||
const headers = ["Country", "PeerId", "Active"];
|
||||
|
||||
const rows =
|
||||
((data as any)?.table?.nodes || []).map((node: any) => (
|
||||
<Row
|
||||
cells={[
|
||||
<PeerCountryCell
|
||||
onPinAdd={onPinAdd}
|
||||
address={node.address}></PeerCountryCell>,
|
||||
<Cell>{node.peerId}</Cell>,
|
||||
<Cell>
|
||||
{node.seen ? (
|
||||
<div className="networkIndicator-point networkIndicator-point--online"></div>
|
||||
) : (
|
||||
<div className="networkIndicator-point networkIndicator-point--offline"></div>
|
||||
)}
|
||||
</Cell>,
|
||||
]}></Row>
|
||||
)) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
|
||||
className="peers-map"
|
||||
/>
|
||||
|
||||
<Table headers={headers} rows={rows} className="peers-table" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user