Skip to content

Commit 732ced7

Browse files
committed
feat(indexd): contract explorer
1 parent f0bb2a8 commit 732ced7

13 files changed

Lines changed: 1001 additions & 15 deletions
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
DataTable,
3+
useDataTable,
4+
useDataTableParams,
5+
} from '@siafoundation/design-system'
6+
import { useCallback } from 'react'
7+
import { contractsColumns } from './contractsColumns'
8+
import { DataViewSelect, type DataView } from '../Views'
9+
import { SidePanel } from '../SidePanel'
10+
import { SidePanelContract } from './SidePanelContract'
11+
import { SidePanelContractList } from './SidePanelContractList'
12+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
13+
import { ContractData } from './types'
14+
import { useContracts } from './useContracts'
15+
16+
export function ContractsTab() {
17+
const params = useSearchParams()
18+
const router = useRouter()
19+
const pathname = usePathname()
20+
const view = params.get('view') as DataView
21+
const setView = (view: DataView) => {
22+
const paramsObj = new URLSearchParams(Array.from(params.entries()))
23+
paramsObj.set('view', view)
24+
router.push(`${pathname}?${paramsObj.toString()}`)
25+
}
26+
const {
27+
offset,
28+
limit,
29+
setOffset,
30+
setLimit,
31+
selectedId,
32+
setSelectedId,
33+
columnFilters,
34+
setColumnFilters,
35+
columnSorts,
36+
setColumnSorts,
37+
} = useDataTableParams('contractList')
38+
const onRowClick = useCallback(
39+
(id: string) => {
40+
setSelectedId(id)
41+
},
42+
[setSelectedId],
43+
)
44+
45+
const contracts = useContracts()
46+
const table = useDataTable<ContractData>({
47+
data: contracts,
48+
columns: contractsColumns,
49+
columnFilters,
50+
setColumnFilters,
51+
columnSorts,
52+
setColumnSorts,
53+
offset,
54+
limit,
55+
onRowClick,
56+
setOffset,
57+
setLimit,
58+
})
59+
60+
return (
61+
<div className="w-full h-full overflow-hidden flex flex-col">
62+
<div className="flex flex-row flex-1 overflow-hidden w-full">
63+
<div className="flex-1 h-full overflow-hidden p-4">
64+
<DataTable
65+
{...table}
66+
heading={<DataViewSelect value={view} onValueChange={setView} />}
67+
/>
68+
</div>
69+
<div className="py-4 pr-4">
70+
{selectedId ? (
71+
<SidePanel showCloseButton onClose={() => setSelectedId(undefined)}>
72+
<SidePanelContract />
73+
</SidePanel>
74+
) : (
75+
<SidePanel
76+
showCloseButton={false}
77+
onClose={() => setSelectedId(undefined)}
78+
>
79+
<SidePanelContractList contracts={table.rows} />
80+
</SidePanel>
81+
)}
82+
</div>
83+
</div>
84+
</div>
85+
)
86+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
Heading,
3+
HostMap,
4+
Separator,
5+
Text,
6+
ValueCopyable,
7+
ValueScFiat,
8+
} from '@siafoundation/design-system'
9+
import { useDataTableParams } from '@siafoundation/design-system'
10+
import { useMemo } from 'react'
11+
import { useContracts } from './useContracts'
12+
import { UsabilityBadges } from '../UsabilityBadges'
13+
import { StateBadge, StatusBadge } from './contractsColumns'
14+
import { InfoRow } from '../InfoRow'
15+
16+
export function SidePanelContract() {
17+
const { selectedId } = useDataTableParams('contractList')
18+
const contracts = useContracts()
19+
const contract = useMemo(
20+
() => contracts.find((c) => c.id === selectedId),
21+
[selectedId, contracts],
22+
)
23+
const mapContract = useMemo(() => {
24+
if (!contract) return null
25+
return {
26+
id: contract.hostKey,
27+
publicKey: contract.hostKey,
28+
location: contract.location,
29+
}
30+
}, [contract])
31+
if (!contract) {
32+
return (
33+
<div className="flex flex-col items-center justify-center overflow-hidden">
34+
<Text>Contract not found</Text>
35+
</div>
36+
)
37+
}
38+
return (
39+
<div className="flex flex-col overflow-hidden">
40+
<Heading size="24" className="mr-[50px] truncate">
41+
Contract: {contract?.id}
42+
</Heading>
43+
<HostMap
44+
hosts={mapContract ? [mapContract] : []}
45+
activeHost={null}
46+
onHostMapClick={() => null}
47+
showLegend={false}
48+
scale={180}
49+
/>
50+
<Separator className="mb-2" />
51+
<Text size="16" weight="medium" className="mb-2">
52+
Info
53+
</Text>
54+
<div className="flex flex-col gap-2">
55+
<InfoRow label="State" value={<StateBadge value={contract.state} />} />
56+
<InfoRow
57+
label="Status"
58+
value={<StatusBadge value={contract.good ? 'Good' : 'Bad'} />}
59+
/>
60+
<InfoRow
61+
label="Host public key"
62+
value={
63+
<ValueCopyable value={contract.hostKey} type="hostPublicKey" />
64+
}
65+
/>
66+
{contract.host?.addresses.map((address) => (
67+
<InfoRow
68+
key={address.address}
69+
label={address.protocol}
70+
value={<ValueCopyable value={address.address} maxLength={24} />}
71+
/>
72+
))}
73+
</div>
74+
<Separator className="mt-4 mb-2" />
75+
<Text size="16" weight="medium" className="mb-2">
76+
Usability
77+
</Text>
78+
<UsabilityBadges
79+
usable={contract.usable}
80+
usability={contract.host?.usability}
81+
className="w-full overflow-hidden flex-wrap"
82+
/>
83+
</div>
84+
)
85+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
Heading,
3+
HostMap,
4+
useDataTableParams,
5+
} from '@siafoundation/design-system'
6+
import { HostMapHost } from '@siafoundation/design-system'
7+
import { ContractData } from './types'
8+
import { Row } from '@tanstack/react-table'
9+
import { useMemo, useDeferredValue } from 'react'
10+
11+
export function SidePanelContractList({
12+
contracts,
13+
}: {
14+
contracts: Row<ContractData>[]
15+
}) {
16+
// Defer the hosts prop to avoid blocking UI on large updates.
17+
const deferredContracts = useDeferredValue(contracts)
18+
const { offset, limit, setSelectedId } = useDataTableParams('contractList')
19+
const mapHosts = useMemo(() => {
20+
if (deferredContracts.length > 100) {
21+
// Group by country
22+
const groupedHosts = deferredContracts.reduce(
23+
(acc, contract) => {
24+
const country = contract.original.location?.countryCode
25+
if (!country || !contract.original.location) {
26+
return acc
27+
}
28+
if (!acc[country]) {
29+
acc[country] = {
30+
type: 'group',
31+
id: contract.original.id,
32+
location: {
33+
latitude: contract.original.location?.latitude,
34+
longitude: contract.original.location?.longitude,
35+
countryCode: contract.original.location?.countryCode,
36+
},
37+
}
38+
}
39+
return acc
40+
},
41+
{} as {
42+
[key: string]: HostMapHost
43+
},
44+
)
45+
return Object.values(groupedHosts)
46+
}
47+
return deferredContracts
48+
.slice(offset, offset + limit)
49+
.map(({ original: contract }) => {
50+
if (!contract.location) {
51+
return null
52+
}
53+
const hostData: HostMapHost = {
54+
id: contract.id,
55+
publicKey: contract.id,
56+
location: {
57+
latitude: contract.location.latitude,
58+
longitude: contract.location.longitude,
59+
countryCode: contract.location.countryCode,
60+
},
61+
}
62+
return hostData
63+
})
64+
.filter((contract) => contract !== null)
65+
}, [deferredContracts, offset, limit])
66+
return (
67+
<div className="flex flex-col overflow-hidden">
68+
<Heading size="24" className="mb-2">
69+
Contracts ({contracts.length})
70+
</Heading>
71+
<HostMap
72+
hosts={mapHosts}
73+
activeHost={null}
74+
onHostMapClick={(id) => setSelectedId(id)}
75+
scale={180}
76+
showLegend={false}
77+
/>
78+
</div>
79+
)
80+
}

0 commit comments

Comments
 (0)