mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-01-05 15:03:06 +00:00
Merge branch 'feat/peers/peers-page' into releases/v0.0.4
This commit is contained in:
commit
677f2fcffb
63
package-lock.json
generated
63
package-lock.json
generated
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
5
src/components/Peers/PeerCountryCell.css
Normal file
5
src/components/Peers/PeerCountryCell.css
Normal file
@ -0,0 +1,5 @@
|
||||
.peerCountry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
73
src/components/Peers/PeerCountryCell.tsx
Normal file
73
src/components/Peers/PeerCountryCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1557
src/components/Peers/countries.ts
Normal file
1557
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) => (
|
||||
|
||||
41
src/routes/dashboard/peers.css
Normal file
41
src/routes/dashboard/peers.css
Normal 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; */
|
||||
}
|
||||
}
|
||||
102
src/routes/dashboard/peers.tsx
Normal file
102
src/routes/dashboard/peers.tsx
Normal 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]];
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 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.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>
|
||||
);
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user