Skip to content

Commit

Permalink
feat: renterd contract metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Dec 7, 2023
1 parent ca76343 commit 42dc75f
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-parrots-fail.md
@@ -0,0 +1,5 @@
---
'renterd': minor
---

Contracts now have a details view that shows a graph of the contract's daily funding and spending.
28 changes: 28 additions & 0 deletions apps/renterd/components/Contracts/ContractMetrics.tsx
@@ -0,0 +1,28 @@
import { ChartXY, Text, stripPrefix } from '@siafoundation/design-system'
import { useContracts } from '../../contexts/contracts'

export function ContractMetrics() {
const { selectedContract, contractMetrics } = useContracts()

if (!selectedContract) {
return null
}

return (
<ChartXY
id="fundingAndSpending"
height="100%"
data={contractMetrics.data}
config={contractMetrics.config}
isLoading={contractMetrics.isLoading}
actionsLeft={
<>
<Text font="mono" weight="semibold">
Contract {stripPrefix(selectedContract.id).slice(0, 6)}: funding &
spending
</Text>
</>
}
/>
)
}
94 changes: 71 additions & 23 deletions apps/renterd/components/Contracts/index.tsx
@@ -1,6 +1,6 @@
import { RenterdSidenav } from '../RenterdSidenav'
import { routes } from '../../config/routes'
import { Table } from '@siafoundation/design-system'
import { ScrollArea, Table } from '@siafoundation/design-system'
import { useDialog } from '../../contexts/dialog'
import { useContracts } from '../../contexts/contracts'
import { RenterdAuthedLayout } from '../RenterdAuthedLayout'
Expand All @@ -9,6 +9,8 @@ import { StateNoneYet } from './StateNoneYet'
import { ContractsActionsMenu } from './ContractsActionsMenu'
import { StateError } from './StateError'
import { ContractsFilterBar } from './ContractsFilterBar'
import { cx } from 'class-variance-authority'
import { ContractMetrics } from './ContractMetrics'

export function Contracts() {
const { openDialog } = useDialog()
Expand All @@ -23,39 +25,85 @@ export function Contracts() {
dataState,
cellContext,
error,
viewMode,
selectedContract,
} = useContracts()

const listHeight =
viewMode === 'detail'
? datasetPage && datasetPage.length
? `${400 - Math.max((2 - datasetPage.length) * 100, 0)}px`
: '400px'
: '100%'

return (
<RenterdAuthedLayout
title="Active contracts"
routes={routes}
sidenav={<RenterdSidenav />}
openSettings={() => openDialog('settings')}
stats={<ContractsFilterBar />}
size="full"
actions={<ContractsActionsMenu />}
size="full"
scroll={false}
>
<div className="p-6 min-w-fit">
<Table
context={cellContext}
isLoading={dataState === 'loading'}
emptyState={
dataState === 'noneMatchingFilters' ? (
<StateNoneMatching />
) : dataState === 'noneYet' ? (
<StateNoneYet />
) : dataState === 'error' ? (
<StateError error={error} />
) : null
}
sortableColumns={sortableColumns}
pageSize={limit}
data={datasetPage}
columns={columns}
sortDirection={sortDirection}
sortField={sortField}
toggleSort={toggleSort}
/>
<div className="relative flex flex-col overflow-hidden h-full w-full">
<div
className={cx(
'absolute w-full',
viewMode === 'detail' ? 'block' : 'invisible',
'transition-all',
'p-6'
)}
style={{
height: `calc(100% - ${listHeight})`,
}}
>
<ContractMetrics />
</div>
<div
className={cx(
'absolute overflow-hidden transition-all w-full',
'duration-300',
'overflow-hidden'
)}
style={{
bottom: 0,
height: listHeight,
}}
>
<ScrollArea className="z-0" id="scroll-hosts">
<div
className={cx(
viewMode === 'detail' ? 'pb-6 px-6' : 'p-6',
'min-w-fit'
)}
>
<Table
context={cellContext}
isLoading={dataState === 'loading'}
emptyState={
dataState === 'noneMatchingFilters' ? (
<StateNoneMatching />
) : dataState === 'noneYet' ? (
<StateNoneYet />
) : dataState === 'error' ? (
<StateError error={error} />
) : null
}
sortableColumns={sortableColumns}
pageSize={limit}
data={datasetPage}
columns={columns}
sortDirection={sortDirection}
sortField={sortField}
toggleSort={toggleSort}
focusId={selectedContract?.id}
rowSize="default"
/>
</div>
</ScrollArea>
</div>
</div>
</RenterdAuthedLayout>
)
Expand Down
175 changes: 172 additions & 3 deletions apps/renterd/contexts/contracts/index.tsx
Expand Up @@ -6,13 +6,32 @@ import {
useClientFilters,
useClientFilteredDataset,
minutesInMilliseconds,
daysInMilliseconds,
Chart,
formatChartData,
computeChartStats,
ValueScFiat,
colors,
getDataIntervalLabelFormatter,
} from '@siafoundation/design-system'
import { useRouter } from 'next/router'
import { useContracts as useContractsData } from '@siafoundation/react-renterd'
import { createContext, useContext, useMemo } from 'react'
import {
useContracts as useContractsData,
useMetricsContract,
} from '@siafoundation/react-renterd'
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
import BigNumber from 'bignumber.js'
import {
ChartContractCategory,
ChartContractKey,
ContractData,
ViewMode,
columnsDefaultVisible,
defaultSortField,
sortOptions,
Expand All @@ -21,10 +40,12 @@ import { columns } from './columns'
import { useSiaCentralHosts } from '@siafoundation/react-sia-central'
import { useSyncStatus } from '../../hooks/useSyncStatus'
import { useSiascanUrl } from '../../hooks/useSiascanUrl'
import { humanSiacoin } from '@siafoundation/sia-js'

const defaultLimit = 50

function useContractsMain() {
const [viewMode, setViewMode] = useState<ViewMode>('list')
const router = useRouter()
const limit = Number(router.query.limit || defaultLimit)
const offset = Number(router.query.offset || 0)
Expand All @@ -43,6 +64,19 @@ function useContractsMain() {
? syncStatus.nodeBlockHeight
: syncStatus.estimatedBlockHeight

const [selectedContractId, setSelectedContractId] = useState<string>()
const selectContract = useCallback(
(id: string) => {
if (selectedContractId === id) {
setSelectedContractId(undefined)
setViewMode('list')
return
}
setSelectedContractId(id)
setViewMode('detail')
},
[selectedContractId, setSelectedContractId, setViewMode]
)
const dataset = useMemo<ContractData[] | null>(() => {
if (!response.data) {
return null
Expand All @@ -57,6 +91,7 @@ function useContractsMain() {
const endTime = blockHeightToTime(currentHeight, endHeight)
return {
id: c.id,
onClick: () => selectContract(c.id),
contractId: c.id,
state: c.state,
hostIp: c.hostIP,
Expand All @@ -81,7 +116,12 @@ function useContractsMain() {
}
}) || []
return data
}, [response.data, geoHosts, currentHeight])
}, [response.data, geoHosts, currentHeight, selectContract])

const selectedContract = useMemo(
() => dataset?.find((d) => d.id === selectedContractId),
[dataset, selectedContractId]
)

const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } =
useClientFilters<ContractData>()
Expand Down Expand Up @@ -151,6 +191,125 @@ function useContractsMain() {
[syncStatus.estimatedBlockHeight, contractsTimeRange, siascanUrl]
)

// don't use exact times, round to 5 minutes so that swr can cache
// if the user flips back and forth between contracts.
const start = getTimeClampedToNearest5min(selectedContract?.startTime || 0)
const interval = daysInMilliseconds(1)
const periods = useMemo(() => {
const now = new Date().getTime()
const today = getTimeClampedToNearest5min(now)
const span = today - start
return Math.round(span / interval)
}, [start, interval])

const contractMetricsResponse = useMetricsContract({
disabled: !selectedContract,
params: {
start: new Date(start).toISOString(),
interval,
n: periods,
contractID: selectedContract?.id,
},
})

const contractMetrics = useMemo<
Chart<ChartContractKey, ChartContractCategory>
>(() => {
const data = formatChartData(
contractMetricsResponse.data
?.map((m) => ({
uploadSpending: Number(m.uploadSpending),
listSpending: Number(m.listSpending),
deleteSpending: Number(m.deleteSpending),
fundAccountSpending: Number(m.fundAccountSpending),
remainingCollateral: Number(m.remainingCollateral),
remainingFunds: Number(m.remainingFunds),
timestamp: new Date(m.timestamp).getTime(),
}))
.slice(1),
'none'
)
const stats = computeChartStats(data)
return {
data,
stats,
config: {
enabledGraph: [
'remainingFunds',
'remainingCollateral',
'fundAccountSpending',
'uploadSpending',
'listSpending',
'deleteSpending',
],
enabledTip: [
'remainingFunds',
'remainingCollateral',
'fundAccountSpending',
'uploadSpending',
'listSpending',
'deleteSpending',
],
categories: ['funding', 'spending'],
data: {
remainingFunds: {
label: 'remaining funds',
category: 'funding',
color: colors.emerald[600],
},
remainingCollateral: {
label: 'remaining collateral',
category: 'funding',
pattern: true,
color: colors.emerald[600],
},
fundAccountSpending: {
label: 'fund account',
category: 'spending',
color: colors.red[600],
},
uploadSpending: {
label: 'upload',
category: 'spending',
color: colors.red[600],
},
listSpending: {
label: 'list',
category: 'spending',
color: colors.red[600],
},
deleteSpending: {
label: 'delete',
category: 'spending',
color: colors.red[600],
},
},
formatComponent: function ({ value }) {
return <ValueScFiat variant="value" value={new BigNumber(value)} />
},
formatTimestamp:
interval === daysInMilliseconds(1)
? getDataIntervalLabelFormatter('daily')
: undefined,
formatTickY: (v) =>
humanSiacoin(v, {
fixed: 0,
dynamicUnits: true,
}),
disableAnimations: true,
chartType: 'barstack',
curveType: 'linear',
stackOffset: 'none',
},
isLoading:
contractMetricsResponse.isValidating && !contractMetricsResponse.data,
}
}, [
contractMetricsResponse.data,
contractMetricsResponse.isValidating,
interval,
])

return {
dataState,
limit,
Expand Down Expand Up @@ -181,6 +340,11 @@ function useContractsMain() {
resetFilters,
sortDirection,
resetDefaultColumnVisibility,
viewMode,
setViewMode,
selectedContract,
selectContract,
contractMetrics,
}
}

Expand All @@ -201,3 +365,8 @@ export function ContractsProvider({ children }: Props) {
</ContractsContext.Provider>
)
}

function getTimeClampedToNearest5min(t: number) {
const granularity = minutesInMilliseconds(5)
return Math.round(t / granularity) * granularity
}

0 comments on commit 42dc75f

Please sign in to comment.