Merge branch 'feat/peers/peers-page' into releases/v0.0.4

This commit is contained in:
Arnaud 2024-10-11 17:33:02 +02:00
commit 677f2fcffb
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
10 changed files with 1880 additions and 3 deletions

63
package-lock.json generated
View File

@ -15,8 +15,8 @@
"@sentry/react": "^8.31.0",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-router": "^1.58.7",
"chart.js": "^4.4.4",
"echarts": "^5.5.1",
"dotted-map": "^2.2.3",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.445.0",
"react": "^18.3.1",
@ -1606,6 +1606,37 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@turf/boolean-point-in-polygon": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
"integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
"dependencies": {
"@turf/helpers": "^6.5.0",
"@turf/invariant": "^6.5.0"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@turf/helpers": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
"integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@turf/invariant": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
"integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
"dependencies": {
"@turf/helpers": "^6.5.0"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"dev": true,
@ -2259,6 +2290,15 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/dotted-map": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/dotted-map/-/dotted-map-2.2.3.tgz",
"integrity": "sha512-8hyOOHHLLVCcCisM3yb9hqp+3bJ7TSMcr1SfrUw8Wxp5UMqih35jIvUyagweCooJbz/EH1nC9GGuPysh7+YlAg==",
"dependencies": {
"@turf/boolean-point-in-polygon": "^6.0.1",
"proj4": "^2.6.1"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"dev": true,
@ -2988,6 +3028,11 @@
"node": ">= 8"
}
},
"node_modules/mgrs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
"integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA=="
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -3238,6 +3283,15 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proj4": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.12.1.tgz",
"integrity": "sha512-vmhP3hmstjXjzFwg8QXJwpoj4n7GVrXk3ZW3DzNK/Ur4cuwXq7ZiMXaWYvLYLQbX8n4MXgbwTr4lthOUZltBpA==",
"dependencies": {
"mgrs": "1.0.0",
"wkt-parser": "^1.3.3"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"dev": true,
@ -4552,6 +4606,11 @@
"node": ">= 8"
}
},
"node_modules/wkt-parser": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz",
"integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw=="
},
"node_modules/word-wrap": {
"version": "1.2.5",
"dev": true,
@ -4603,4 +4662,4 @@
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
}
}

View File

@ -30,8 +30,8 @@
"@sentry/react": "^8.31.0",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-router": "^1.58.7",
"chart.js": "^4.4.4",
"echarts": "^5.5.1",
"dotted-map": "^2.2.3",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.445.0",
"react": "^18.3.1",

View File

@ -0,0 +1,5 @@
.peerCountry {
display: flex;
align-items: center;
gap: 1rem;
}

View File

@ -0,0 +1,73 @@
import { Cell } from "@codex-storage/marketplace-ui-components";
import { PeerPin } from "./types";
import { countriesCoordinates } from "./countries";
import { useQuery } from "@tanstack/react-query";
import "./PeerCountryCell.css";
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 { data } = useQuery({
queryFn: () => {
const [ip] = address.split(":");
return fetch(import.meta.env.VITE_GEO_IP_URL + "/" + ip)
.then((res) => res.json())
.then((json) => {
const coordinate = countriesCoordinates.find(
(c) => c.iso === json.country
);
if (coordinate) {
onPinAdd({
lat: parseFloat(coordinate.lat),
lng: parseFloat(coordinate.lng),
});
}
return coordinate;
});
},
queryKey: [address],
// Enable only when the address exists
enabled: !!address,
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// We can cache the data at Infinity because the relation between
// country and ip is fixed
staleTime: Infinity,
// Don't expect something new when coming back to the UI
refetchOnWindowFocus: false,
});
return (
<Cell>
<div className="peerCountry">
{data ? (
<>
<span> {!!data && getFlagEmoji(data.iso)}</span>
<span>{data?.name}</span>
</>
) : (
<span>{address}</span>
)}
</div>
</Cell>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
export type PeerPin = {
lat: number;
lng: number;
};

View File

@ -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"

View File

@ -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) => (

View File

@ -0,0 +1,41 @@
.peers-map {
max-width: 1000px;
width: 100%;
}
.peers-table {
margin-top: 1rem;
width: calc(100% - 4rem);
max-width: calc(1000px - 4rem);
}
.peers {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 4rem;
padding-left: 2rem;
padding-right: 2rem;
}
.peers circle[fill="#d6ff79"] {
/* fill: yellow; */
animation: dash 3s linear infinite;
stroke: white;
stroke-width: 0.6px;
stroke-dasharray: 0.3;
}
@keyframes dash {
from {
stroke-dashoffset: 2;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes circleAn {
to {
/* stroke-dashoffset: 100px; */
}
}

View File

@ -0,0 +1,102 @@
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]];
});
}, []);
// Its safe to re-create the map at each render, because of the
// pre-computation its 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.8 * 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 (
<div className="peers">
{/* <img
src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
className="peers-map"
/> */}
<div
className="peers-map"
dangerouslySetInnerHTML={{ __html: svgMap }}></div>
<Table headers={headers} rows={rows} className="peers-table" />
</div>
);
},
});