diff --git a/.changeset/famous-queens-protect.md b/.changeset/famous-queens-protect.md new file mode 100644 index 000000000..cfb622a64 --- /dev/null +++ b/.changeset/famous-queens-protect.md @@ -0,0 +1,5 @@ +--- +'explorer': minor +--- + +Fixed an issue loading the host page for hosts that are not benchmarked yet. diff --git a/.changeset/few-steaks-fail.md b/.changeset/few-steaks-fail.md new file mode 100644 index 000000000..b354e5e82 --- /dev/null +++ b/.changeset/few-steaks-fail.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The maxDowntimeHours setting default value is now 336. diff --git a/.changeset/funny-crabs-vanish.md b/.changeset/funny-crabs-vanish.md new file mode 100644 index 000000000..f73f47e5c --- /dev/null +++ b/.changeset/funny-crabs-vanish.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +Added support for the minRecentScanFailures autopilot hosts setting. diff --git a/.changeset/hot-humans-cover.md b/.changeset/hot-humans-cover.md new file mode 100644 index 000000000..eeae6b150 --- /dev/null +++ b/.changeset/hot-humans-cover.md @@ -0,0 +1,5 @@ +--- +'website': minor +--- + +The navbar now includes the logo and "sia" as text. diff --git a/.changeset/itchy-lizards-speak.md b/.changeset/itchy-lizards-speak.md new file mode 100644 index 000000000..a31de8463 --- /dev/null +++ b/.changeset/itchy-lizards-speak.md @@ -0,0 +1,7 @@ +--- +'hostd': minor +'renterd': minor +'@siafoundation/design-system': minor +--- + +Extremely small siacoin values will now show as hastings by default rather than 0SC. diff --git a/.changeset/lemon-dragons-hug.md b/.changeset/lemon-dragons-hug.md new file mode 100644 index 000000000..d4d18d4ab --- /dev/null +++ b/.changeset/lemon-dragons-hug.md @@ -0,0 +1,5 @@ +--- +'website': minor +--- + +The website now features an improved interactive host map. diff --git a/.changeset/tender-crabs-peel.md b/.changeset/tender-crabs-peel.md new file mode 100644 index 000000000..cb32e1a7c --- /dev/null +++ b/.changeset/tender-crabs-peel.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +Refined the warnings in the files feature navbar and file explorer empty states. diff --git a/.changeset/yellow-files-approve.md b/.changeset/yellow-files-approve.md new file mode 100644 index 000000000..2b19f26ca --- /dev/null +++ b/.changeset/yellow-files-approve.md @@ -0,0 +1,5 @@ +--- +'explorer': minor +--- + +Fixed an issue where transaction values were incorrect on the address page transaction list. diff --git a/README.md b/README.md index 5ec30d767..b3d551626 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The Sia web libraries provide developers with convenient TypeScript SDKs for usi - [@siafoundation/sia-nodejs](libs/sia-nodejs) - Sia NodeJS client for controlling a v1 `siad`. - [@siafoundation/design-system](libs/design-system) - React-based design system used across Sia apps and websites. - [@siafoundation/data-sources](libs/data-sources) - Data sources used for stats on the website. +- [@siafoundation/units](libs/units) - Methods and types for converting and displaying units. ## Internal diff --git a/apps/explorer/app/host/[id]/opengraph-image.tsx b/apps/explorer/app/host/[id]/opengraph-image.tsx index a53d460e0..aa6fe199b 100644 --- a/apps/explorer/app/host/[id]/opengraph-image.tsx +++ b/apps/explorer/app/host/[id]/opengraph-image.tsx @@ -6,10 +6,12 @@ import { getOGImage } from '../../../components/OGImageEntity' import { siaCentralApi } from '../../../config' import { getDownloadCost, + getDownloadSpeed, getStorageCost, getUploadCost, -} from '../../../lib/host' -import { humanBytes, humanSpeed } from '@siafoundation/sia-js' + getUploadSpeed, +} from '@siafoundation/units' +import { humanBytes } from '@siafoundation/sia-js' import { truncate } from '@siafoundation/design-system' import { CurrencyOption, currencyOptions } from '@siafoundation/react-core' @@ -76,10 +78,7 @@ export default async function Image({ params }) { rate: r.rates.sc.usd, }, }), - subvalue: humanSpeed( - (h.host.benchmark.data_size * 8) / - (h.host.benchmark.download_time / 1000) - ), + subvalue: h.host.benchmark && getDownloadSpeed(h.host), }, { label: 'upload', @@ -90,9 +89,7 @@ export default async function Image({ params }) { rate: r.rates.sc.usd, }, }), - subvalue: humanSpeed( - (h.host.benchmark.data_size * 8) / (h.host.benchmark.upload_time / 1000) - ), + subvalue: h.host.benchmark && getUploadSpeed(h.host), }, ] diff --git a/apps/explorer/components/Address/index.tsx b/apps/explorer/components/Address/index.tsx index 7af7fede8..03cd87213 100644 --- a/apps/explorer/components/Address/index.tsx +++ b/apps/explorer/components/Address/index.tsx @@ -52,10 +52,12 @@ export function Address({ id, address }: Props) { ...address.unconfirmed_transactions.map((tx) => ({ hash: tx.id, sc: getTotal({ + address: id, inputs: tx.siacoin_inputs, outputs: tx.siacoin_outputs, }), sf: getTotal({ + address: id, inputs: tx.siafund_inputs, outputs: tx.siafund_outputs, }).toNumber(), @@ -72,10 +74,12 @@ export function Address({ id, address }: Props) { ...address.transactions.map((tx) => ({ hash: tx.id, sc: getTotal({ + address: id, inputs: tx.siacoin_inputs, outputs: tx.siacoin_outputs, }), sf: getTotal({ + address: id, inputs: tx.siafund_inputs, outputs: tx.siafund_outputs, }).toNumber(), @@ -88,7 +92,7 @@ export function Address({ id, address }: Props) { ) } return list - }, [address]) + }, [id, address]) const utxos = useMemo(() => { const list: EntityListItemProps[] = [] @@ -159,15 +163,23 @@ export function Address({ id, address }: Props) { } function getTotal({ + address, inputs, outputs, }: { - inputs?: { value: string }[] - outputs?: { value: string }[] + address: string + inputs?: { value: string; unlock_hash: string }[] + outputs?: { value: string; unlock_hash: string }[] }) { return (outputs || []) - .reduce((acc, o) => acc.plus(o.value), new BigNumber(0)) + .reduce( + (acc, o) => (o.unlock_hash === address ? acc.plus(o.value) : acc), + new BigNumber(0) + ) .minus( - (inputs || []).reduce((acc, i) => acc.plus(i.value), new BigNumber(0)) + (inputs || []).reduce( + (acc, i) => (i.unlock_hash === address ? acc.plus(i.value) : acc), + new BigNumber(0) + ) ) } diff --git a/apps/explorer/components/Home/HostListItem.tsx b/apps/explorer/components/Home/HostListItem.tsx index c039d75de..8a5423242 100644 --- a/apps/explorer/components/Home/HostListItem.tsx +++ b/apps/explorer/components/Home/HostListItem.tsx @@ -24,7 +24,7 @@ import { getStorageCost, getUploadCost, getUploadSpeed, -} from '../../lib/host' +} from '@siafoundation/units' import { useMemo } from 'react' import { routes } from '../../config/routes' import { useExchangeRate } from '../../hooks/useExchangeRate' @@ -106,7 +106,7 @@ export function HostListItem({ host, rates, entity }: Props) { - {downloadSpeed} + {downloadSpeed || '-'} @@ -121,7 +121,7 @@ export function HostListItem({ host, rates, entity }: Props) { - {uploadSpeed} + {uploadSpeed || '-'} diff --git a/apps/explorer/components/Home/index.tsx b/apps/explorer/components/Home/index.tsx index 02555c5ef..3b7383624 100644 --- a/apps/explorer/components/Home/index.tsx +++ b/apps/explorer/components/Home/index.tsx @@ -18,7 +18,11 @@ import { SiaCentralHostsNetworkMetricsResponse, } from '@siafoundation/sia-central' import { hashToAvatar } from '../../lib/avatar' -import { getDownloadCost, getStorageCost, getUploadCost } from '../../lib/host' +import { + getDownloadCost, + getStorageCost, + getUploadCost, +} from '@siafoundation/units' import { HostListItem } from './HostListItem' import { useExchangeRate } from '../../hooks/useExchangeRate' @@ -62,7 +66,7 @@ export function Home({ {humanBytes( metrics?.totals.total_storage - - metrics?.totals.remaining_storage + metrics?.totals.remaining_storage )} diff --git a/apps/explorer/components/Host/HostPricing.tsx b/apps/explorer/components/Host/HostPricing.tsx index 3b1346e02..7bd8cb48a 100644 --- a/apps/explorer/components/Host/HostPricing.tsx +++ b/apps/explorer/components/Host/HostPricing.tsx @@ -19,7 +19,7 @@ import { getStorageCost, getUploadCost, getUploadSpeed, -} from '../../lib/host' +} from '@siafoundation/units' import { useExchangeRate } from '../../hooks/useExchangeRate' type Props = { diff --git a/apps/explorer/components/Host/HostSettings.tsx b/apps/explorer/components/Host/HostSettings.tsx index c14f6fe52..64a12ef60 100644 --- a/apps/explorer/components/Host/HostSettings.tsx +++ b/apps/explorer/components/Host/HostSettings.tsx @@ -14,7 +14,11 @@ import { } from '@siafoundation/design-system' import BigNumber from 'bignumber.js' import { humanBytes, humanSiacoin, toSiacoins } from '@siafoundation/sia-js' -import { getDownloadCost, getStorageCost, getUploadCost } from '../../lib/host' +import { + getDownloadCost, + getStorageCost, + getUploadCost, +} from '@siafoundation/units' import { useExchangeRate } from '../../hooks/useExchangeRate' import { siacoinToFiat } from '../../lib/currency' diff --git a/apps/renterd/components/Files/EmptyState/index.tsx b/apps/renterd/components/Files/EmptyState/index.tsx index 96c6c20ee..6ef9378ed 100644 --- a/apps/renterd/components/Files/EmptyState/index.tsx +++ b/apps/renterd/components/Files/EmptyState/index.tsx @@ -1,4 +1,4 @@ -import { LinkButton, Text } from '@siafoundation/design-system' +import { Code, LinkButton, Text } from '@siafoundation/design-system' import { CloudUpload32 } from '@siafoundation/react-icons' import { routes } from '../../../config/routes' import { useFiles } from '../../../contexts/files' @@ -35,12 +35,13 @@ export function EmptyState() {
- Before you can upload files you must configure autopilot. Autopilot - finds contracts with hosts based on the settings you choose. - Autopilot also repairs your data as hosts come and go. + Before you can upload files you must configure your settings. Once + configured, renterd will find contracts with hosts + based on the settings you choose. renterd will also + repair your data as hosts come and go. - Configure autopilot → + Configure
diff --git a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx index 4d4ca657c..ad2f80658 100644 --- a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx +++ b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx @@ -1,42 +1,9 @@ -import { Link, Text, Tooltip } from '@siafoundation/design-system' +import { Text, Tooltip } from '@siafoundation/design-system' import { Warning16 } from '@siafoundation/react-icons' -import { useFiles } from '../../../contexts/files' -import { routes } from '../../../config/routes' import { useContractSetMismatch } from '../checks/useContractSetMismatch' -import { useDefaultContractSetNotSet } from '../checks/useDefaultContractSetNotSet' -import { useAutopilotNotConfigured } from '../checks/useAutopilotNotConfigured' -import { useNotEnoughContracts } from '../checks/useNotEnoughContracts' export function FilesStatsMenuWarnings() { - const { dataState, isViewingRootOfABucket, isViewingBuckets } = useFiles() const contractSetMismatch = useContractSetMismatch() - const defaultContractSetNotSet = useDefaultContractSetNotSet() - const autopilotNotConfigured = useAutopilotNotConfigured() - const notEnoughContracts = useNotEnoughContracts() - - // onboard/warn about default contract set - if (defaultContractSetNotSet.active) { - return ( -
- - - - - Configure a default contract set to get started.{' '} - - Configuration → - - -
- ) - } // warn about contract set mismatch if (contractSetMismatch.active) { @@ -65,66 +32,5 @@ export function FilesStatsMenuWarnings() { ) } - const autopilotNotConfiguredViewingBuckets = - autopilotNotConfigured.active && isViewingBuckets - const autopilotNotConfiguredRootDirectory = - autopilotNotConfigured.active && - isViewingRootOfABucket && - dataState !== 'noneYet' - const autopilotNotConfiguredNotRootDirectory = - autopilotNotConfigured.active && !isViewingRootOfABucket - if ( - autopilotNotConfiguredViewingBuckets || - autopilotNotConfiguredRootDirectory || - autopilotNotConfiguredNotRootDirectory - ) { - return ( -
- - - - - Configure autopilot to get started.{' '} - - Autopilot → - - -
- ) - } - - const notEnoughContractsViewingBuckets = - notEnoughContracts.active && isViewingBuckets - const notEnoughContractsRootDirectoryAndExistingFiles = - notEnoughContracts.active && - isViewingRootOfABucket && - dataState !== 'noneYet' - const notEnoughContractsNotRootDirectory = - notEnoughContracts.active && !isViewingRootOfABucket - if ( - notEnoughContractsViewingBuckets || - notEnoughContractsRootDirectoryAndExistingFiles || - notEnoughContractsNotRootDirectory - ) { - return ( -
- - - - - Not enought contracts to upload files. {notEnoughContracts.count}/ - {notEnoughContracts.required} - -
- ) - } - return null } diff --git a/apps/renterd/components/Hosts/HostMap/HostItem.tsx b/apps/renterd/components/Hosts/HostMap/HostItem.tsx index a822ed138..17ece1bcd 100644 --- a/apps/renterd/components/Hosts/HostMap/HostItem.tsx +++ b/apps/renterd/components/Hosts/HostMap/HostItem.tsx @@ -7,10 +7,11 @@ import { Tooltip, countryCodeEmoji, } from '@siafoundation/design-system' -import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js' +import { humanBytes, humanSiacoin } from '@siafoundation/sia-js' import { cx } from 'class-variance-authority' import BigNumber from 'bignumber.js' import { SiaCentralHost } from '@siafoundation/sia-central' +import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units' type Host = SiaCentralHost @@ -96,18 +97,8 @@ export function HostItem({ {humanBytes(host.settings.total_storage)} - - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.download_time / 1000) - )}{' '} - - - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.upload_time / 1000) - )}{' '} - + {getDownloadSpeed(host)} + {getUploadSpeed(host)}
{storageCost} @@ -149,12 +140,10 @@ export function HostItem({ host.public_key === activeHost?.public_key ? 'semibold' : 'regular' } > - {humanBytes(host.settings.total_storage)} ·{' '} - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.download_time / 1000) - )}{' '} - · {storageCost} + {humanBytes(host.settings.total_storage)} + {host.benchmark && ` · ${getDownloadSpeed(host)}`} + {' · '} + {storageCost} diff --git a/apps/renterd/components/OnboardingBar.tsx b/apps/renterd/components/OnboardingBar.tsx index 64459fc7d..4a493ded8 100644 --- a/apps/renterd/components/OnboardingBar.tsx +++ b/apps/renterd/components/OnboardingBar.tsx @@ -51,7 +51,9 @@ export function OnboardingBar() { return null } - const walletBalance = new BigNumber(wallet.data?.confirmed || 0) + const walletBalance = new BigNumber( + wallet.data ? wallet.data.confirmed + wallet.data.unconfirmed : 0 + ) const allowance = new BigNumber(autopilot.data?.contracts.allowance || 0) const step1Configured = app.autopilot.state.data?.configured diff --git a/apps/renterd/contexts/config/fields.tsx b/apps/renterd/contexts/config/fields.tsx index 0ea009b6d..83dbcaa2c 100644 --- a/apps/renterd/contexts/config/fields.tsx +++ b/apps/renterd/contexts/config/fields.tsx @@ -244,6 +244,28 @@ export function getFields({ } : {}, }, + minRecentScanFailures: { + type: 'number', + category: 'hosts', + title: 'Min recent scan failures', + description: ( + <> + The minimum number of recent scan failures that autopilot will + tolerate. + + ), + units: 'scans', + decimalsLimit: 0, + suggestion: advancedDefaultAutopilot.minRecentScanFailures, + suggestionTip: `Defaults to ${advancedDefaultAutopilot.minRecentScanFailures.toNumber()}.`, + hidden: !isAutopilotEnabled || !showAdvanced, + validation: + isAutopilotEnabled && showAdvanced + ? { + required: 'required', + } + : {}, + }, // wallet defragThreshold: { diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx index 4d4d465b5..36ae3862d 100644 --- a/apps/renterd/contexts/config/index.tsx +++ b/apps/renterd/contexts/config/index.tsx @@ -473,17 +473,26 @@ export function useConfigMain() { throw Error(configAppResponse.error) } - triggerSuccessToast('Configuration has been saved.') if (isAutopilotEnabled) { + // Sync default contract set if necessary. Only syncs if the setting + // is enabled in case the user changes in advanced mode and then + // goes back to simple mode. + // Might be simpler nice to just override in simple mode without a + // special setting since this is how other settings like allowance + // behave - but leaving for now. syncDefaultContractSet(finalValues.autopilotContractSet) + + // Trigger the autopilot loop with new settings applied. autopilotTrigger.post({ payload: { - forceScan: false, + forceScan: true, }, }) } - // if autopilot is being configured for the first time, + triggerSuccessToast('Configuration has been saved.') + + // If autopilot is being configured for the first time, // revalidate the empty hosts list. if (firstTimeSettingConfig) { const refreshHostsAfterDelay = async () => { diff --git a/apps/renterd/contexts/config/transform.spec.ts b/apps/renterd/contexts/config/transform.spec.ts index 942ee5c46..184215dc7 100644 --- a/apps/renterd/contexts/config/transform.spec.ts +++ b/apps/renterd/contexts/config/transform.spec.ts @@ -28,6 +28,7 @@ describe('tansforms', () => { hosts: { allowRedundantIPs: false, maxDowntimeHours: 1440, + minRecentScanFailures: 10, scoreOverrides: null, }, contracts: { @@ -75,6 +76,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), defaultContractSet: 'myset', uploadPackingEnabled: true, @@ -105,6 +107,7 @@ describe('tansforms', () => { hosts: { allowRedundantIPs: false, maxDowntimeHours: 1440, + minRecentScanFailures: 10, scoreOverrides: null, }, contracts: { @@ -152,6 +155,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), defaultContractSet: 'myset', uploadPackingEnabled: true, @@ -188,6 +192,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), }, undefined @@ -199,6 +204,7 @@ describe('tansforms', () => { hosts: { allowRedundantIPs: false, maxDowntimeHours: 1440, + minRecentScanFailures: 10, scoreOverrides: null, }, contracts: { @@ -228,6 +234,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), }, { @@ -255,6 +262,7 @@ describe('tansforms', () => { foobar: 'value', allowRedundantIPs: false, maxDowntimeHours: 1440, + minRecentScanFailures: 10, scoreOverrides: null, }, contracts: { @@ -303,6 +311,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), defaultContractSet: 'myset', uploadPackingEnabled: false, @@ -356,6 +365,7 @@ describe('tansforms', () => { storageTB: new BigNumber('1'), allowRedundantIPs: false, maxDowntimeHours: new BigNumber('1440'), + minRecentScanFailures: new BigNumber('10'), defragThreshold: new BigNumber('1000'), defaultContractSet: 'myset', uploadPackingEnabled: false, @@ -514,6 +524,7 @@ function buildAllResponses() { hosts: { allowRedundantIPs: false, maxDowntimeHours: 1440, + minRecentScanFailures: 10, scoreOverrides: null, }, contracts: { diff --git a/apps/renterd/contexts/config/transform.ts b/apps/renterd/contexts/config/transform.ts index 95eab430a..25f7ab56d 100644 --- a/apps/renterd/contexts/config/transform.ts +++ b/apps/renterd/contexts/config/transform.ts @@ -85,6 +85,7 @@ export function transformUpAutopilot( hosts: { ...existingValues?.hosts, maxDowntimeHours: v.maxDowntimeHours.toNumber(), + minRecentScanFailures: v.minRecentScanFailures.toNumber(), allowRedundantIPs: v.allowRedundantIPs, scoreOverrides: existingValues?.hosts.scoreOverrides || null, }, @@ -241,6 +242,7 @@ export function transformDownAutopilot( // hosts allowRedundantIPs: config.hosts.allowRedundantIPs, maxDowntimeHours: new BigNumber(config.hosts.maxDowntimeHours), + minRecentScanFailures: new BigNumber(config.hosts.minRecentScanFailures), // wallet defragThreshold: new BigNumber(config.wallet.defragThreshold), } diff --git a/apps/renterd/contexts/config/types.ts b/apps/renterd/contexts/config/types.ts index 148316812..88c886d51 100644 --- a/apps/renterd/contexts/config/types.ts +++ b/apps/renterd/contexts/config/types.ts @@ -16,6 +16,7 @@ export const defaultAutopilot = { // hosts allowRedundantIPs: false, maxDowntimeHours: undefined as BigNumber | undefined, + minRecentScanFailures: undefined as BigNumber | undefined, // wallet defragThreshold: undefined as BigNumber | undefined, } @@ -84,7 +85,8 @@ export const advancedDefaultAutopilot: AutopilotData = { amountHosts: new BigNumber(50), autopilotContractSet: 'autopilot', allowRedundantIPs: false, - maxDowntimeHours: new BigNumber(1440), + maxDowntimeHours: new BigNumber(336), + minRecentScanFailures: new BigNumber(10), defragThreshold: new BigNumber(1000), } diff --git a/apps/website/assets/earth-dark-contrast.png b/apps/website/assets/earth-dark-contrast.png new file mode 100644 index 000000000..16056c7a7 Binary files /dev/null and b/apps/website/assets/earth-dark-contrast.png differ diff --git a/apps/website/assets/earth-topology.png b/apps/website/assets/earth-topology.png new file mode 100644 index 000000000..666be0091 Binary files /dev/null and b/apps/website/assets/earth-topology.png differ diff --git a/apps/website/assets/night-sky.png b/apps/website/assets/night-sky.png new file mode 100644 index 000000000..f5d4f2beb Binary files /dev/null and b/apps/website/assets/night-sky.png differ diff --git a/apps/website/components/HostMap/Globe.tsx b/apps/website/components/HostMap/Globe.tsx index 34c18ad83..96eee6506 100644 --- a/apps/website/components/HostMap/Globe.tsx +++ b/apps/website/components/HostMap/Globe.tsx @@ -11,15 +11,15 @@ import dynamic from 'next/dynamic' import { GlobeMethods } from 'react-globe.gl' import { random, sortBy } from 'lodash' import { getHostLabel } from './utils' -import { Host } from '../../content/geoHosts' +import { SiaCentralPartialHost } from '../../content/geoHosts' import { getAssetUrl } from '../../content/assets' import { useTheme } from 'next-themes' import { useElementSize } from 'usehooks-ts' type Props = { - activeHost: Host + activeHost: SiaCentralPartialHost selectActiveHost: (public_key: string) => void - hosts: Host[] + hosts: SiaCentralPartialHost[] rates: { usd: string } @@ -27,8 +27,8 @@ type Props = { type Route = { distance: number - src: Host - dst: Host + src: SiaCentralPartialHost + dst: SiaCentralPartialHost } const GlobeGl = dynamic(() => import('./GlobeGl'), { @@ -44,7 +44,7 @@ const ReactGlobe = forwardRef(function ReactGlobe( function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) { const globeEl = useRef(null) - const moveToHost = useCallback((host: Host) => { + const moveToHost = useCallback((host: SiaCentralPartialHost) => { globeEl.current?.pointOfView( { lat: host.location[0] - 8, @@ -175,19 +175,21 @@ function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) { }} arcsTransitionDuration={0} pointsData={hosts} - pointLat={(h: Host) => h.location[0]} - pointLng={(h: Host) => h.location[1]} - pointLabel={(h: Host) => getHostLabel({ host: h, rates })} + pointLat={(h: SiaCentralPartialHost) => h.location[0]} + pointLng={(h: SiaCentralPartialHost) => h.location[1]} + pointLabel={(h: SiaCentralPartialHost) => + getHostLabel({ host: h, rates }) + } pointAltitude={0} - pointColor={(h: Host) => + pointColor={(h: SiaCentralPartialHost) => h.public_key === activeHost.public_key ? 'rgba(0,255,0,1)' : 'rgba(0,255,0,1)' } - pointRadius={(h: Host) => + pointRadius={(h: SiaCentralPartialHost) => h.public_key === activeHost.public_key ? 0.6 : 0.2 } - onPointClick={(h: Host) => { + onPointClick={(h: SiaCentralPartialHost) => { selectActiveHost(h.public_key) }} pointsMerge={false} @@ -198,14 +200,20 @@ function GlobeComponent({ activeHost, hosts, rates, selectActiveHost }: Props) { export const Globe = memo(GlobeComponent) -function distanceBetweenHosts(h1: Host, h2: Host) { +function distanceBetweenHosts( + h1: SiaCentralPartialHost, + h2: SiaCentralPartialHost +) { return Math.sqrt( Math.pow(h1.location[0] - h2.location[0], 2) + Math.pow(h1.location[1] - h2.location[1], 2) ) } -function doesIncludeActiveHost(route: Route, activeHost: Host) { +function doesIncludeActiveHost( + route: Route, + activeHost: SiaCentralPartialHost +) { return ( route.dst.public_key === activeHost.public_key || route.src.public_key === activeHost.public_key diff --git a/apps/website/components/HostMap/HostItem.tsx b/apps/website/components/HostMap/HostItem.tsx index 77daf8171..eafb65148 100644 --- a/apps/website/components/HostMap/HostItem.tsx +++ b/apps/website/components/HostMap/HostItem.tsx @@ -6,15 +6,19 @@ import { Text, Tooltip, countryCodeEmoji, + LinkButton, + webLinks, } from '@siafoundation/design-system' -import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js' +import { humanBytes, humanSiacoin } from '@siafoundation/sia-js' import { cx } from 'class-variance-authority' import BigNumber from 'bignumber.js' -import { Host } from '../../content/geoHosts' +import { SiaCentralPartialHost } from '../../content/geoHosts' +import { Launch16 } from '@siafoundation/react-icons' +import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units' type Props = { - host: Host - activeHost: Host + host: SiaCentralPartialHost + activeHost: SiaCentralPartialHost setRef?: (el: HTMLButtonElement) => void selectActiveHost: (public_key: string) => void rates: { @@ -81,9 +85,20 @@ export function HostItem({ - - {countryCodeEmoji(host.country_code)} {host.country_code} - +
+ + {countryCodeEmoji(host.country_code)} {host.country_code} + + + + +
storage @@ -94,18 +109,8 @@ export function HostItem({ {humanBytes(host.settings.total_storage)} - - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.download_time / 1000) - )}{' '} - - - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.upload_time / 1000) - )}{' '} - + {getDownloadSpeed(host)} + {getUploadSpeed(host)}
{storageCost} @@ -146,12 +151,10 @@ export function HostItem({ host.public_key === activeHost?.public_key ? 'semibold' : 'regular' } > - {humanBytes(host.settings.total_storage)} ·{' '} - {humanSpeed( - (host.benchmark.data_size * 8) / - (host.benchmark.download_time / 1000) - )}{' '} - · {storageCost} + {humanBytes(host.settings.total_storage)} + {host.benchmark && ` · ${getDownloadSpeed(host)}`} + {' · '} + {storageCost} diff --git a/apps/website/components/HostMap/index.tsx b/apps/website/components/HostMap/index.tsx index 73ed1e099..b7886dae5 100644 --- a/apps/website/components/HostMap/index.tsx +++ b/apps/website/components/HostMap/index.tsx @@ -12,10 +12,10 @@ import { random, throttle } from 'lodash' import { HostItem } from './HostItem' import { Globe } from './Globe' import { cx } from 'class-variance-authority' -import { Host } from '../../content/geoHosts' +import { SiaCentralPartialHost } from '../../content/geoHosts' type Props = { - hosts: Host[] + hosts: SiaCentralPartialHost[] rates: { usd: string diff --git a/apps/website/components/HostMap/utils.ts b/apps/website/components/HostMap/utils.ts index f7f539814..0568a9b0f 100644 --- a/apps/website/components/HostMap/utils.ts +++ b/apps/website/components/HostMap/utils.ts @@ -3,15 +3,16 @@ import { TBToBytes, countryCodeEmoji, } from '@siafoundation/design-system' -import { humanBytes, humanSiacoin, humanSpeed } from '@siafoundation/sia-js' +import { humanBytes, humanSiacoin } from '@siafoundation/sia-js' import BigNumber from 'bignumber.js' -import { Host } from '../../content/geoHosts' +import { SiaCentralPartialHost } from '../../content/geoHosts' +import { getDownloadSpeed } from '@siafoundation/units' export function getHostLabel({ host, rates, }: { - host: Host + host: SiaCentralPartialHost rates: { usd: string } @@ -32,7 +33,5 @@ export function getHostLabel({ return `${countryCodeEmoji(host.country_code)} · ${humanBytes( host.settings.total_storage - )} · ${humanSpeed( - (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000) - )} · ${storageCost}` + )}${host.benchmark && ` · ${getDownloadSpeed(host)}`} · ${storageCost}` } diff --git a/apps/website/components/Layout/Navbar/index.tsx b/apps/website/components/Layout/Navbar/index.tsx index 8ad2ecce5..19e1ddeab 100644 --- a/apps/website/components/Layout/Navbar/index.tsx +++ b/apps/website/components/Layout/Navbar/index.tsx @@ -12,10 +12,16 @@ import { menuSections } from '../../../config/siteMap' import { SiteMenu } from '../SiteMenu' import { NavMenu } from './NavMenu' -export function Navbar({ scrolledDown }: { scrolledDown: boolean }) { +export function Navbar({ + size = '4', + scrolledDown, +}: { + size?: '4' | 'full' + scrolledDown: boolean +}) { return ( - - + + + + sia +
diff --git a/apps/website/components/Map/Globe.tsx b/apps/website/components/Map/Globe.tsx new file mode 100644 index 000000000..ccd2de29f --- /dev/null +++ b/apps/website/components/Map/Globe.tsx @@ -0,0 +1,206 @@ +import { useEffect, useRef, useCallback, useMemo } from 'react' +import { GlobeMethods } from 'react-globe.gl' +import { getHostLabel } from './utils' +import { useElementSize } from 'usehooks-ts' +import { useTryUntil } from '@siafoundation/react-core' +import earthDarkContrast from '../../assets/earth-dark-contrast.png' +import earthTopology from '../../assets/earth-topology.png' +import nightSky from '../../assets/night-sky.png' +import { GlobeDyn } from './GlobeDyn' +import { useSiaCentralExchangeRates } from '@siafoundation/react-sia-central' +import { colors } from '@siafoundation/design-system' +import { useDecRoutes } from './useRoutes' +import { SiaCentralPartialHost } from '../../content/geoHosts' + +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?: SiaCentralPartialHost + hosts?: SiaCentralPartialHost[] + onHostClick: (public_key: string, location: [number, number]) => void + onHostHover: (public_key: string, location: [number, number]) => void + onMount?: (cmd: Commands) => void +} + +type Route = { + distance: number + src: SiaCentralPartialHost + dst: SiaCentralPartialHost +} + +export function Globe({ + activeHost, + hosts, + onMount, + onHostClick, + onHostHover, +}: Props) { + const rates = useSiaCentralExchangeRates({ + config: { + swr: { + revalidateOnFocus: false, + }, + }, + }) + const globeEl = useRef(null) + const cmdRef = useRef(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 [containerRef, { height, width }] = useElementSize() + + const points = useMemo(() => hosts || [], [hosts]) + + return ( +
+ + 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.public_key) + // }} + arcsTransitionDuration={0} + pointsData={points} + pointLat={(h: SiaCentralPartialHost) => h.location[0]} + pointLng={(h: SiaCentralPartialHost) => h.location[1]} + pointLabel={(h: SiaCentralPartialHost) => + getHostLabel({ host: h, rates: rates.data?.rates.sc }) + } + // pointAltitude={ + // (h: SiaCentralPartialHost) => h.settings.remainingstorage / 1e13 / 100 + // // h.public_key === activeHost.public_key ? 0.6 : 0.2 + // } + pointAltitude={(h: SiaCentralPartialHost) => { + return 0.05 + }} + pointsTransitionDuration={0} + pointColor={(h: SiaCentralPartialHost) => { + return activeHost?.public_key === h.public_key + ? colors.green[600] + : colorWithOpacity(colors.green[200], 1) + }} + pointRadius={(h: SiaCentralPartialHost) => { + let radius = 0 + radius = h.settings.total_storage / 1e13 / 3 + return Math.max(radius, 0.1) + }} + onPointHover={(h: SiaCentralPartialHost) => { + if (!h) { + return + } + onHostHover?.(h.public_key, h.location) + }} + onPointClick={(h: SiaCentralPartialHost) => { + if (!h) { + return + } + onHostClick?.(h.public_key, h.location) + }} + pointsMerge={false} + /> +
+ ) +} + +function doesIncludeActiveHost( + route: Route, + activeHost?: SiaCentralPartialHost +) { + if (!activeHost) { + return false + } + return ( + route.dst.public_key === activeHost.public_key || + route.src.public_key === activeHost.public_key + ) +} + +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})` +} diff --git a/apps/website/components/Map/GlobeDyn.tsx b/apps/website/components/Map/GlobeDyn.tsx new file mode 100644 index 000000000..7c4214a05 --- /dev/null +++ b/apps/website/components/Map/GlobeDyn.tsx @@ -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, 'forwardRef'>, + ref: MutableRefObject +) { + return +}) diff --git a/apps/website/components/Map/GlobeImp.tsx b/apps/website/components/Map/GlobeImp.tsx new file mode 100644 index 000000000..af0372b6c --- /dev/null +++ b/apps/website/components/Map/GlobeImp.tsx @@ -0,0 +1,11 @@ +import { MutableRefObject } from 'react' +import GlobeTmpl, { GlobeMethods } from 'react-globe.gl' + +const GlobeImp = ({ + forwardRef, + ...otherProps +}: React.ComponentProps & { + forwardRef: MutableRefObject +}) => + +export default GlobeImp diff --git a/apps/website/components/Map/HostItem.tsx b/apps/website/components/Map/HostItem.tsx new file mode 100644 index 000000000..191fab1c6 --- /dev/null +++ b/apps/website/components/Map/HostItem.tsx @@ -0,0 +1,144 @@ +import { useMemo } from 'react' +import { + Button, + monthsToBlocks, + TBToBytes, + Text, + countryCodeEmoji, + LinkButton, + webLinks, +} from '@siafoundation/design-system' +import { humanBytes, humanSiacoin } from '@siafoundation/sia-js' +import { cx } from 'class-variance-authority' +import BigNumber from 'bignumber.js' +import { SiaCentralPartialHost } from '../../content/geoHosts' +import { Launch16 } from '@carbon/icons-react' +import { getDownloadSpeed, getUploadSpeed } from '@siafoundation/units' + +type Props = { + host: SiaCentralPartialHost + activeHost: SiaCentralPartialHost + setRef?: (el: HTMLButtonElement) => void + selectActiveHost: (public_key: string) => void + rates: { + usd: string + } +} + +export function HostItem({ + host, + activeHost, + selectActiveHost, + setRef, + rates, +}: Props) { + const storageCost = useMemo( + () => + rates + ? `$${new BigNumber(host.settings.storage_price) + .times(TBToBytes(1)) + .times(monthsToBlocks(1)) + .div(1e24) + .times(rates?.usd || 1) + .toFixed(2)}/TB` + : `${humanSiacoin( + new BigNumber(host.settings.storage_price) + .times(TBToBytes(1)) + .times(monthsToBlocks(1)), + { fixed: 0 } + )}/TB`, + [rates, host] + ) + + const downloadCost = useMemo( + () => + rates + ? `$${new BigNumber(host.settings.download_price) + .times(TBToBytes(1)) + .div(1e24) + .times(rates?.usd || 1) + .toFixed(2)}/TB` + : `${humanSiacoin( + new BigNumber(host.settings.download_price).times(TBToBytes(1)), + { fixed: 0 } + )}/TB`, + [rates, host] + ) + + const uploadCost = useMemo( + () => + rates + ? `$${new BigNumber(host.settings.upload_price) + .times(TBToBytes(1)) + .div(1e24) + .times(rates?.usd || 1) + .toFixed(2)}/TB` + : `${humanSiacoin( + new BigNumber(host.settings.upload_price).times(TBToBytes(1)), + { fixed: 0 } + )}/TB`, + [rates, host] + ) + + return ( + + ) +} diff --git a/apps/website/components/Map/index.tsx b/apps/website/components/Map/index.tsx new file mode 100644 index 000000000..cf01a86b2 --- /dev/null +++ b/apps/website/components/Map/index.tsx @@ -0,0 +1,176 @@ +import { useAppSettings } from '@siafoundation/react-core' +import { Globe } from './Globe' +import { useCallback, useMemo, useRef, useState } from 'react' +import { ScrollArea, Text, Tooltip } from '@siafoundation/design-system' +import { HostItem } from './HostItem' +import { Navbar } from '../Layout/Navbar' +import { SiaCentralPartialHost } from '../../content/geoHosts' +import { PageHead } from '../PageHead' +import { previews } from '../../content/assets' +import { Stats } from '../../content/stats' + +export type Commands = { + moveToLocation: ( + location: [number, number] | undefined, + altitude?: number + ) => void +} + +export const emptyCommands: Commands = { + moveToLocation: (location: [number, number], altitude?: number) => null, +} + +type Props = { + hosts: SiaCentralPartialHost[] + rates: { usd: string } + stats: Stats +} + +export function Map({ hosts, rates, stats }: Props) { + const { gpu, settings } = useAppSettings() + + const [activeHostPublicKey, setActiveHostPublicKey] = useState( + hosts[0].public_key + ) + + const activeHost = useMemo( + () => hosts?.find((d) => d.public_key === activeHostPublicKey), + [hosts, activeHostPublicKey] + ) + + const cmdRef = useRef(emptyCommands) + + const setCmd = useCallback( + (cmd: Commands) => { + cmdRef.current = cmd + }, + [cmdRef] + ) + + const scrollToHost = useCallback((publicKey: string) => { + // move table to host, select via data id data-table + const selectedElement = document.getElementById(publicKey) + const scrollContainer = document.getElementById('scroll-hosts') + + const containerWidth = scrollContainer.offsetWidth + const selectedElementWidth = selectedElement.offsetWidth + const selectedElementLeft = selectedElement.offsetLeft + + const scrollPosition = + selectedElementLeft - containerWidth / 2 + selectedElementWidth / 2 + scrollContainer.scroll({ + left: scrollPosition, + behavior: 'smooth', + }) + }, []) + + const onHostMapClick = useCallback( + (publicKey: string, location?: [number, number]) => { + if (activeHostPublicKey === publicKey) { + setActiveHostPublicKey(undefined) + return + } + setActiveHostPublicKey(publicKey) + if (location) { + cmdRef.current.moveToLocation(location) + } + scrollToHost(publicKey) + }, + [setActiveHostPublicKey, cmdRef, activeHostPublicKey, scrollToHost] + ) + + const onHostListClick = useCallback( + (publicKey: string, location?: [number, number]) => { + if (activeHostPublicKey === publicKey) { + setActiveHostPublicKey(undefined) + return + } + setActiveHostPublicKey(publicKey) + if (location) { + cmdRef.current.moveToLocation(location) + } + scrollToHost(publicKey) + }, + [setActiveHostPublicKey, cmdRef, activeHostPublicKey, scrollToHost] + ) + + const onHostMapHover = useCallback( + (publicKey: string, location?: [number, number]) => null, + [] + ) + + if (settings.siaCentral && !gpu.shouldRender) { + return null + } + + return ( +
+ + +
+ { + setCmd(cmd) + }} + /> +
+
+
+
+ + + {stats.activeHosts} hosts + + + + + {stats.totalStorage} + + +
+ +
+ {hosts.map((h, i) => ( +
+ + onHostListClick(h.public_key, h.location) + } + rates={rates} + /> +
+ ))} +
+
+
+
+
+ ) +} diff --git a/apps/website/components/Map/useRoutes.tsx b/apps/website/components/Map/useRoutes.tsx new file mode 100644 index 000000000..13e83233b --- /dev/null +++ b/apps/website/components/Map/useRoutes.tsx @@ -0,0 +1,92 @@ +import { useMemo } from 'react' +import { random, sortBy } from 'lodash' +import { SiaCentralPartialHost } from '../../content/geoHosts' + +type Props = { + activeHost?: SiaCentralPartialHost + hosts?: SiaCentralPartialHost[] +} + +type Route = { + distance: number + src: SiaCentralPartialHost + dst: SiaCentralPartialHost +} + +export function useDecRoutes({ activeHost, hosts }: Props) { + const backgroundRoutes = useMemo(() => { + const routes: Route[] = [] + if (!hosts) { + return routes + } + for (let i = 0; i < hosts.length; i++) { + const host1 = hosts[i] + let hostRoutes: Route[] = [] + for (let j = 0; j < hosts.length; j++) { + if (i === j) { + continue + } + const host2 = hosts[j] + const distance = distanceBetweenHosts(host1, host2) + hostRoutes.push({ + distance, + src: host1, + dst: host2, + }) + } + hostRoutes = sortBy(hostRoutes, 'distance') + const closest = hostRoutes.slice(4, 6) + routes.push(...closest) + + const addExtra = Math.random() < 0.1 + if (addExtra) { + const randomDistantIndex = random( + // Math.round((hostRoutes.length - 1) / 2), + 0, + hostRoutes.length - 1 + ) + const extra = hostRoutes[randomDistantIndex] + routes.push(extra) + } + } + return routes + }, [hosts]) + + const activeRoutes = useMemo(() => { + let routes: Route[] = [] + if (!hosts || !activeHost) { + return routes + } + for (let i = 0; i < hosts.length; i++) { + const host = hosts[i] + if (activeHost.public_key === host.public_key) { + continue + } + const distance = distanceBetweenHosts(activeHost, host) + routes.push({ + distance, + src: activeHost, + dst: host, + }) + } + routes = sortBy(routes, 'distance') + return routes.slice(0, 5) + }, [activeHost, hosts]) + + const routes = useMemo( + () => [...backgroundRoutes, ...activeRoutes], + [backgroundRoutes, activeRoutes] + ) + + return routes +} + +function distanceBetweenHosts( + h1: SiaCentralPartialHost, + h2: SiaCentralPartialHost +) { + return Math.sqrt( + Math.pow(h1.location[0] - h2.location[0], 2) + + Math.pow(h1.location[1] - h2.location[1], 2) + ) +} diff --git a/apps/website/components/Map/utils.ts b/apps/website/components/Map/utils.ts new file mode 100644 index 000000000..e811e5049 --- /dev/null +++ b/apps/website/components/Map/utils.ts @@ -0,0 +1,44 @@ +import { + monthsToBlocks, + TBToBytes, + countryCodeEmoji, +} from '@siafoundation/design-system' +import { humanBytes, humanSiacoin } from '@siafoundation/sia-js' +import { SiaCentralPartialHost } from '../../content/geoHosts' +import BigNumber from 'bignumber.js' + +export function getHostLabel({ + host, + rates, +}: { + host: SiaCentralPartialHost + rates?: { + usd: string + } +}) { + const storageCost = rates + ? `$${new BigNumber(host.settings.storage_price) + .times(TBToBytes(1)) + .times(monthsToBlocks(1)) + .div(1e24) + .times(rates?.usd || 1) + .toFixed(2)}/TB` + : `${humanSiacoin( + new BigNumber(host.settings.storage_price) + .times(TBToBytes(1)) + .times(monthsToBlocks(1)), + { fixed: 0 } + )}/TB` + + const usedStorage = `${humanBytes( + host.settings.total_storage - host.settings.remaining_storage + )} utilized` + + const availableStorage = `${humanBytes( + host.settings.remaining_storage + )} / ${humanBytes(host.settings.total_storage)} available` + + return `${countryCodeEmoji( + host.country_code + )} · ${storageCost} · ${usedStorage} · ${availableStorage}` +} diff --git a/apps/website/content/geoHosts.ts b/apps/website/content/geoHosts.ts index 4da6c2f55..a69cbf042 100644 --- a/apps/website/content/geoHosts.ts +++ b/apps/website/content/geoHosts.ts @@ -7,14 +7,14 @@ const maxAge = getMinutesInSeconds(5) const maxHosts = 1000 const minDegreesApart = 1 -export async function getGeoHosts(): Promise { +export async function getGeoHosts(): Promise { return getCacheValue( 'geoHosts', async () => { const { data: siaCentralHosts, error } = await getSiaCentralHosts({ params: { - limit: 300 - } + limit: 300, + }, }) if (error) { return [] @@ -23,7 +23,7 @@ export async function getGeoHosts(): Promise { hosts.sort((a, b) => a.settings.total_storage - a.settings.remaining_storage < - b.settings.total_storage - a.settings.remaining_storage + b.settings.total_storage - a.settings.remaining_storage ? 1 : -1 ) @@ -43,9 +43,9 @@ export async function getGeoHosts(): Promise { for (const hostToDisplay of hostsToDisplay) { if ( Math.abs(hostToDisplay.location[0] - host.location[0]) < - minDegreesApart && + minDegreesApart && Math.abs(hostToDisplay.location[1] - host.location[1]) < - minDegreesApart + minDegreesApart ) { unique = false break @@ -65,7 +65,7 @@ export async function getGeoHosts(): Promise { ) } -export type Host = { +export type SiaCentralPartialHost = { public_key: string country_code: string location: [number, number] @@ -74,8 +74,9 @@ export type Host = { download_price: string upload_price: string total_storage: number + remaining_storage: number } - benchmark: { + benchmark?: { data_size: number download_time: number upload_time: number @@ -83,7 +84,7 @@ export type Host = { } // transform to only necessary data to limit transfer size -export function transformHost(h: SiaCentralHost): Host { +export function transformHost(h: SiaCentralHost): SiaCentralPartialHost { return { public_key: h.public_key, country_code: h.country_code, @@ -93,11 +94,12 @@ export function transformHost(h: SiaCentralHost): Host { download_price: h.settings.download_price, upload_price: h.settings.upload_price, total_storage: h.settings.total_storage, + remaining_storage: h.settings.remaining_storage, }, benchmark: { - data_size: h.benchmark.data_size, - download_time: h.benchmark.download_time, - upload_time: h.benchmark.upload_time, + data_size: h.benchmark?.data_size || null, + download_time: h.benchmark?.download_time || null, + upload_time: h.benchmark?.upload_time || null, }, } } diff --git a/apps/website/pages/globe.tsx b/apps/website/pages/map.tsx similarity index 56% rename from apps/website/pages/globe.tsx rename to apps/website/pages/map.tsx index 1337bcd6e..b5a922eb6 100644 --- a/apps/website/pages/globe.tsx +++ b/apps/website/pages/map.tsx @@ -1,22 +1,27 @@ import { AsyncReturnType } from '../lib/types' -import { HostMap } from '../components/HostMap' import { getGeoHosts } from '../content/geoHosts' import { getExchangeRates } from '../content/exchangeRates' import { getMinutesInSeconds } from '../lib/time' +import { Map } from '../components/Map' +import { getStats } from '../content/stats' type Props = AsyncReturnType['props'] -export default function Globe({ hosts, rates }: Props) { - return +export default function MapPage({ hosts, rates, stats }: Props) { + return } export async function getStaticProps() { - const hosts = await getGeoHosts() - const { data: rates } = await getExchangeRates() + const [hosts, { data: rates }, stats] = await Promise.all([ + getGeoHosts(), + getExchangeRates(), + getStats(), + ]) const props = { hosts, rates: rates?.rates.sc, + stats, } return { diff --git a/libs/design-system/src/components/ValueSc.tsx b/libs/design-system/src/components/ValueSc.tsx index 5813d0eb4..54e20093f 100644 --- a/libs/design-system/src/components/ValueSc.tsx +++ b/libs/design-system/src/components/ValueSc.tsx @@ -13,6 +13,7 @@ type Props = { tooltip?: string fixed?: number dynamicUnits?: boolean + hastingUnits?: boolean extendedSuffix?: string showTooltip?: boolean } @@ -25,6 +26,7 @@ export function ValueSc({ variant = 'change', fixed = 3, dynamicUnits = true, + hastingUnits = true, extendedSuffix, showTooltip = true, }: Props) { @@ -52,7 +54,7 @@ export function ValueSc({ fixed, dynamicUnits, })}` - : humanSiacoin(value, { fixed, dynamicUnits })} + : humanSiacoin(value, { fixed, dynamicUnits, hastingUnits })} {extendedSuffix ? `${extendedSuffix}` : ''} diff --git a/libs/design-system/src/components/Wordmark.tsx b/libs/design-system/src/components/Wordmark.tsx index 6ec83d671..fdd3930df 100644 --- a/libs/design-system/src/components/Wordmark.tsx +++ b/libs/design-system/src/components/Wordmark.tsx @@ -1,14 +1,32 @@ -import wordmark from '../assets/wordmark.svg' -import { Image } from '../core/Image' +type Props = { + size?: number + color: string + className?: string +} -export function Wordmark({ size = 20 }) { +export function Wordmark({ size = 20, className, color = '#333333' }: Props) { return ( - Sia + viewBox="0 0 65 39" + className={className} + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + + + + ) } diff --git a/libs/react-renterd/src/siaTypes.ts b/libs/react-renterd/src/siaTypes.ts index e4e38ff42..06cda247f 100644 --- a/libs/react-renterd/src/siaTypes.ts +++ b/libs/react-renterd/src/siaTypes.ts @@ -346,6 +346,7 @@ export interface AutopilotHostsConfig { allowRedundantIPs: boolean scoreOverrides: { [key: PublicKey]: number } maxDowntimeHours: number + minRecentScanFailures: number } export interface AutopilotContractsConfig { diff --git a/libs/units/.babelrc b/libs/units/.babelrc new file mode 100644 index 000000000..1ea870ead --- /dev/null +++ b/libs/units/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/units/.eslintrc.json b/libs/units/.eslintrc.json new file mode 100644 index 000000000..1e06b264c --- /dev/null +++ b/libs/units/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/libs/units/CHANGELOG.md b/libs/units/CHANGELOG.md new file mode 100644 index 000000000..7fa4e4862 --- /dev/null +++ b/libs/units/CHANGELOG.md @@ -0,0 +1 @@ +# @siafoundation/units diff --git a/libs/units/README.md b/libs/units/README.md new file mode 100644 index 000000000..85e79a04d --- /dev/null +++ b/libs/units/README.md @@ -0,0 +1,7 @@ +# units + +Methods and types for converting and displaying units. + +## Running unit tests + +Run `nx test units` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/units/jest.config.ts b/libs/units/jest.config.ts new file mode 100644 index 000000000..e014298d9 --- /dev/null +++ b/libs/units/jest.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +export default { + displayName: 'units', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/units', +} diff --git a/libs/units/package.json b/libs/units/package.json new file mode 100644 index 000000000..f967574e9 --- /dev/null +++ b/libs/units/package.json @@ -0,0 +1,14 @@ +{ + "name": "@siafoundation/units", + "description": "Methods and types for interacting with the Sia Central API.", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@siafoundation/design-system": "^0.58.1", + "@siafoundation/sia-central": "^0.1.0", + "bignumber.js": "^9.0.2", + "@siafoundation/sia-js": "^0.11.0", + "@siafoundation/react-core": "^0.15.0" + }, + "types": "./src/index.d.ts" +} diff --git a/libs/units/project.json b/libs/units/project.json new file mode 100644 index 000000000..c917025e2 --- /dev/null +++ b/libs/units/project.json @@ -0,0 +1,48 @@ +{ + "name": "units", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/units/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/units", + "tsConfig": "libs/units/tsconfig.lib.json", + "project": "libs/units/package.json", + "entryFile": "libs/units/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nx/react/plugins/bundle-rollup", + "compiler": "swc", + "assets": [ + { + "glob": "libs/units/*.md", + "input": ".", + "output": "." + } + ] + }, + "configurations": {} + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "libs/units/**/*.{ts,tsx,js,jsx}", + "libs/units/package.json" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/units"], + "options": { + "jestConfig": "libs/units/jest.config.ts", + "passWithNoTests": true + } + } + } +} diff --git a/apps/explorer/lib/host.ts b/libs/units/src/index.ts similarity index 77% rename from apps/explorer/lib/host.ts rename to libs/units/src/index.ts index c23c282ce..28174b118 100644 --- a/apps/explorer/lib/host.ts +++ b/libs/units/src/index.ts @@ -50,16 +50,28 @@ export function getUploadCost({ price, exchange }: Props) { })}/TB` } -export function getDownloadSpeed(host: SiaCentralHost) { - return humanSpeed( - (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000) - ) +type SiaCentralPartialHost = { + benchmark?: { + data_size: number + download_time: number + upload_time: number + } +} + +export function getDownloadSpeed(host: SiaCentralPartialHost) { + return host.benchmark + ? humanSpeed( + (host.benchmark.data_size * 8) / (host.benchmark.download_time / 1000) + ) + : '-' } -export function getUploadSpeed(host: SiaCentralHost) { - return humanSpeed( - (host.benchmark.data_size * 8) / (host.benchmark.upload_time / 1000) - ) +export function getUploadSpeed(host: SiaCentralPartialHost) { + return host.benchmark + ? humanSpeed( + (host.benchmark.data_size * 8) / (host.benchmark.upload_time / 1000) + ) + : '-' } export function getRemainingOverTotalStorage(host: SiaCentralHost) { diff --git a/libs/units/tsconfig.json b/libs/units/tsconfig.json new file mode 100644 index 000000000..4c089585e --- /dev/null +++ b/libs/units/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/units/tsconfig.lib.json b/libs/units/tsconfig.lib.json new file mode 100644 index 000000000..621db72d7 --- /dev/null +++ b/libs/units/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "jest.config.ts" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/units/tsconfig.spec.json b/libs/units/tsconfig.spec.json new file mode 100644 index 000000000..a85d573fc --- /dev/null +++ b/libs/units/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index c9c06d882..816901039 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,7 +27,8 @@ "@siafoundation/react-walletd": ["libs/react-walletd/src/index.ts"], "@siafoundation/sia-central": ["libs/sia-central/src/index.ts"], "@siafoundation/sia-js": ["libs/sia-js/src/index.ts"], - "@siafoundation/sia-nodejs": ["libs/sia-nodejs/src/index.ts"] + "@siafoundation/sia-nodejs": ["libs/sia-nodejs/src/index.ts"], + "@siafoundation/units": ["libs/units/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]