Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

renterd interactive host explorer #352

Merged
merged 1 commit into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/four-dolls-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Table now supports an active row with colored border.
5 changes: 5 additions & 0 deletions .changeset/hungry-baboons-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

AppAuthedLayout now has a scroll prop.
5 changes: 5 additions & 0 deletions .changeset/nervous-dogs-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/react-core': minor
---

Added useTryUntil.
5 changes: 5 additions & 0 deletions .changeset/new-geckos-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The host explorer now has an interactive map. Filtered hosts can be selected via the list or map, and hosts on the map are colored based on whether renterd is actively contracting with the host. For hosts with active contracts the size of the hosts on the map is based on renterd's used storage, for hosts without active contracts the size is based on the hosts remaining storage.
5 changes: 5 additions & 0 deletions .changeset/red-dolls-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/react-core': minor
---

Add useSiaCentralHosts.
5 changes: 5 additions & 0 deletions .changeset/slimy-files-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Fixed border issues with table.
5 changes: 5 additions & 0 deletions .changeset/thirty-countries-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Add box-shadow based border classes.
Binary file added apps/renterd/assets/earth-dark-contrast.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/earth-dark.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/earth-day.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/earth-night.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/earth-topology.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/map-2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/renterd/assets/night-sky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion apps/renterd/components/Hosts/HostContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ export function HostContextMenu({
<Draggable16 />
</Button>
}
contentProps={{ align: 'start', ...contentProps }}
contentProps={{
align: 'start',
...contentProps,
onClick: (e) => {
e.stopPropagation()
},
}}
>
<div className="px-1.5 py-1">
<Text size="14" weight="medium" color="subtle">
Expand Down
224 changes: 224 additions & 0 deletions apps/renterd/components/Hosts/HostMap/Globe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { useEffect, useRef, useCallback, useMemo } from 'react'
import { GlobeMethods } from 'react-globe.gl'
import { getHostLabel } from './utils'
import { useElementSize } from 'usehooks-ts'
import {
useSiaCentralMarketExchangeRate,
useTryUntil,
} from '@siafoundation/react-core'
import earthDarkContrast from '../../../assets/earth-dark-contrast.png'
import earthTopology from '../../../assets/earth-topology.png'
import { GlobeDyn } from './GlobeDyn'
import { HostDataWithLocation } from '../../../contexts/hosts/types'
import BigNumber from 'bignumber.js'
import { getHostStatus } from '../../../contexts/hosts/status'

export type Commands = {
moveToLocation: (
location: [number, number] | undefined,
altitude?: number
) => void
}

export const emptyCommands: Commands = {
moveToLocation: (location: [number, number], altitude?: number) => null,
}

type Props = {
activeHost?: HostDataWithLocation
hosts?: HostDataWithLocation[]
onHostClick: (publicKey: string, location: [number, number]) => void
onHostHover: (publicKey: string, location: [number, number]) => void
onMount?: (cmd: Commands) => void
}

type Route = {
distance: number
src: HostDataWithLocation
dst: HostDataWithLocation
}

export function Globe({
activeHost,
hosts,
onMount,
onHostClick,
onHostHover,
}: Props) {
const rates = useSiaCentralMarketExchangeRate({
config: {
swr: {
revalidateOnFocus: false,
},
},
})
const globeEl = useRef<GlobeMethods>(null)
const cmdRef = useRef<Commands>(emptyCommands)
const moveToLocation = useCallback(
(location: [number, number] | undefined, altitude?: number) => {
if (!location) {
return
}
globeEl.current?.pointOfView(
{
lat: location[0] - 8,
lng: location[1],
altitude: altitude || 1.5,
},
700
)
},
[]
)

useEffect(() => {
cmdRef.current.moveToLocation = moveToLocation
}, [moveToLocation])

useTryUntil(() => {
if (!globeEl.current) {
return false
}

moveToLocation(activeHost?.location || [48.8323, 2.4075], 1.5)

const directionalLight = globeEl.current
?.scene()
.children.find((obj3d) => obj3d.type === 'DirectionalLight')
if (directionalLight) {
// directionalLight.position.set(1, 1, 1)
directionalLight.intensity = 10
}
return true
})

useEffect(() => {
if (onMount) {
onMount(cmdRef.current)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// const routes = useDecRoutes({ hosts, activeHost })
const routes = []

const [containerRef, { height, width }] = useElementSize()

const points = useMemo(() => hosts || [], [hosts])

return (
<div ref={containerRef} className="w-full h-full">
<GlobeDyn
ref={globeEl}
width={width}
height={height}
backgroundColor="rgba(0,0,0,0)"
globeImageUrl={earthDarkContrast.src}
bumpImageUrl={earthTopology.src}
// backgroundImageUrl={nightSky.src}
arcsData={routes}
atmosphereColor="rgba(0,0,0,0)"
atmosphereAltitude={0.16}
animateIn={false}
arcLabel={(r: Route) =>
getHostLabel({ host: r.dst, rates: rates.data?.rates.sc })
}
arcStartLat={(r: Route) => +r.src.location[0]}
arcStartLng={(r: Route) => +r.src.location[1]}
arcEndLat={(r: Route) => +r.dst.location[0]}
arcEndLng={(r: Route) => +r.dst.location[1]}
arcDashLength={0.75}
arcAltitude={0}
arcDashGap={0.1}
arcDashInitialGap={() => Math.random()}
arcDashAnimateTime={5000}
// arcDashAnimateTime={(route: Route) =>
// doesIncludeActiveHost(route, activeHost) ? 5000 : 0
// }
arcColor={(r: Route) =>
doesIncludeActiveHost(r, activeHost)
? [`rgba(187, 229, 201, 0.25)`, `rgba(187, 229, 201, 0.25)`]
: [`rgba(187, 229, 201, 0.10)`, `rgba(187, 229, 201, 0.10)`]
}
// onArcClick={(r: Route) => {
// selectActiveHost(r.dst.publicKey)
// }}
arcsTransitionDuration={0}
pointsData={points}
pointLat={(h: HostDataWithLocation) => h.location[0]}
pointLng={(h: HostDataWithLocation) => h.location[1]}
pointLabel={(h: HostDataWithLocation) =>
getHostLabel({ host: h, rates: rates.data?.rates.sc })
}
// pointAltitude={
// (h: HostDataWithLocation) => h.settings.remainingstorage / 1e13 / 100
// // h.publicKey === activeHost.publicKey ? 0.6 : 0.2
// }
pointAltitude={(h: HostDataWithLocation) => {
if (activeHost) {
return h.publicKey === activeHost?.publicKey
? 0.1
: h.activeContractsCount.gt(0)
? 0.1
: 0.1
}
return h.activeContractsCount.gt(0) ? 0.1 : 0.1
}}
pointsTransitionDuration={0}
pointColor={(h: HostDataWithLocation) => {
const { color } = getHostStatus(h)
if (!activeHost || h.publicKey === activeHost?.publicKey) {
return color
}
return colorWithOpacity(color, 0.2)
}}
pointRadius={(h: HostDataWithLocation) => {
let radius = 0
if (h.activeContractsCount.gt(0)) {
radius = h.activeContracts
.reduce((acc, c) => acc.plus(c.size), new BigNumber(0))
.div(1e12)
.toNumber()
}
radius = h.settings.remainingstorage / 1e13 / 3

return Math.max(radius, 0.1)
}}
onPointHover={(h: HostDataWithLocation) => {
if (!h) {
return
}
onHostHover?.(h.publicKey, h.location)
}}
onPointClick={(h: HostDataWithLocation) => {
if (!h) {
return
}
onHostClick?.(h.publicKey, h.location)
}}
pointsMerge={false}
/>
</div>
)
}

function doesIncludeActiveHost(
route: Route,
activeHost?: HostDataWithLocation
) {
if (!activeHost) {
return false
}
return (
route.dst.publicKey === activeHost.publicKey ||
route.src.publicKey === activeHost.publicKey
)
}

function colorWithOpacity(hexColor: string, opacity: number) {
const r = parseInt(hexColor.slice(1, 3), 16)
const g = parseInt(hexColor.slice(3, 5), 16)
const b = parseInt(hexColor.slice(5, 7), 16)

return `rgba(${r}, ${g}, ${b}, ${opacity})`
}
14 changes: 14 additions & 0 deletions apps/renterd/components/Hosts/HostMap/GlobeDyn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { forwardRef, MutableRefObject } from 'react'
import dynamic from 'next/dynamic'
import { GlobeMethods } from 'react-globe.gl'

const GlobeGl = dynamic(() => import('./GlobeImp'), {
ssr: false,
})

export const GlobeDyn = forwardRef(function ReactGlobe(
props: Omit<React.ComponentProps<typeof GlobeGl>, 'forwardRef'>,
ref: MutableRefObject<GlobeMethods>
) {
return <GlobeGl {...props} forwardRef={ref} />
})
11 changes: 11 additions & 0 deletions apps/renterd/components/Hosts/HostMap/GlobeImp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MutableRefObject } from 'react'
import GlobeTmpl, { GlobeMethods } from 'react-globe.gl'

const GlobeImp = ({
forwardRef,
...otherProps
}: React.ComponentProps<typeof GlobeTmpl> & {
forwardRef: MutableRefObject<GlobeMethods>
}) => <GlobeTmpl {...otherProps} ref={forwardRef} />

export default GlobeImp
Loading