diff --git a/.changeset/funny-zebras-double.md b/.changeset/funny-zebras-double.md new file mode 100644 index 000000000..03c9cf675 --- /dev/null +++ b/.changeset/funny-zebras-double.md @@ -0,0 +1,8 @@ +--- +'explorer': minor +'hostd': minor +'renterd': minor +'walletd': minor +--- + +Copyable entity values now have a context menu with support for opening Siascan pages. diff --git a/apps/hostd/components/Profile/index.tsx b/apps/hostd/components/Profile/index.tsx index 5b3ce4ee7..cfcf51818 100644 --- a/apps/hostd/components/Profile/index.tsx +++ b/apps/hostd/components/Profile/index.tsx @@ -12,6 +12,7 @@ import { } from '@siafoundation/react-hostd' import { useSyncStatus } from '../../hooks/useSyncStatus' import { useDialog } from '../../contexts/dialog' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' export function Profile() { const { openDialog } = useDialog() @@ -31,6 +32,7 @@ export function Profile() { }) const syncStatus = useSyncStatus() const peers = useSyncerPeers() + const siascanUrl = useSiascanUrl() const version = state.data?.version const versionUrl = version?.match(/^v\d+\.\d+\.\d+/) @@ -48,49 +50,50 @@ export function Profile() { firstTimeSyncing={syncStatus.firstTimeSyncing} moreThan100BlocksToSync={syncStatus.moreThan100BlocksToSync} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -98,7 +101,7 @@ export function Profile() { {state.data?.network}
-
+
diff --git a/apps/hostd/contexts/contracts/columns.tsx b/apps/hostd/contexts/contracts/columns.tsx index bc019e590..94fe71bfa 100644 --- a/apps/hostd/contexts/contracts/columns.tsx +++ b/apps/hostd/contexts/contracts/columns.tsx @@ -26,6 +26,7 @@ type Context = { startHeight: number endHeight: number } + siascanUrl: string } type ContractsTableColumn = TableColumn< @@ -52,14 +53,15 @@ export const columns: ContractsTableColumn[] = ( id: 'contractId', label: 'contract ID', category: 'general', - render: ({ data }) => { + render: ({ data, context }) => { const { id, renewedFrom, isRenewedFrom, renewedTo, isRenewedTo } = data return (
{isRenewedFrom && ( @@ -70,8 +72,9 @@ export const columns: ContractsTableColumn[] = (
@@ -86,7 +89,8 @@ export const columns: ContractsTableColumn[] = ( color="subtle" size="10" value={stripPrefix(renewedTo)} - label="contract ID" + type="contract" + siascanUrl={context.siascanUrl} />
diff --git a/apps/hostd/contexts/contracts/index.tsx b/apps/hostd/contexts/contracts/index.tsx index 4404ec7b6..1746afca5 100644 --- a/apps/hostd/contexts/contracts/index.tsx +++ b/apps/hostd/contexts/contracts/index.tsx @@ -21,6 +21,7 @@ import { import { columns } from './columns' import { useDataset } from './dataset' import { useSyncStatus } from '../../hooks/useSyncStatus' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' const defaultLimit = 50 @@ -92,14 +93,22 @@ function useContractsMain() { [currentHeight, dataset] ) + const siascanUrl = useSiascanUrl() + + const cellContext = useMemo( + () => ({ + contractsTimeRange, + currentHeight, + siascanUrl, + }), + [contractsTimeRange, currentHeight, siascanUrl] + ) + return { dataState, offset, limit, - cellContext: { - contractsTimeRange, - currentHeight, - }, + cellContext, pageCount: dataset?.length || 0, totalCount: response.data?.count, columns: filteredTableColumns, diff --git a/apps/hostd/contexts/transactions/index.tsx b/apps/hostd/contexts/transactions/index.tsx index 80d60f80b..c09cb4276 100644 --- a/apps/hostd/contexts/transactions/index.tsx +++ b/apps/hostd/contexts/transactions/index.tsx @@ -1,5 +1,5 @@ import { - EntityListItemProps, + TxType, daysInMilliseconds, getTransactionType, secondsInMilliseconds, @@ -14,10 +14,30 @@ import { createContext, useContext, useMemo } from 'react' import { useDialog } from '../dialog' import BigNumber from 'bignumber.js' import { useRouter } from 'next/router' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' const defaultLimit = 50 const filters = [] +export type TransactionDataPending = { + type: 'transaction' + txType: TxType + siascanUrl: string +} + +export type TransactionDataConfirmed = { + type: 'transaction' + txType: TxType + hash: string + timestamp: number + onClick: () => void + unconfirmed: boolean + sc: BigNumber + siascanUrl: string +} + +export type TransactionData = TransactionDataPending | TransactionDataConfirmed + function useTransactionsMain() { const router = useRouter() const limit = Number(router.query.limit || defaultLimit) @@ -42,13 +62,15 @@ function useTransactionsMain() { }) const { openDialog } = useDialog() + const siascanUrl = useSiascanUrl() - const dataset: EntityListItemProps[] | null = useMemo(() => { + const dataset: TransactionData[] | null = useMemo(() => { if (!pending.data || !transactions.data) { return null } return [ - ...(pending.data || []).map((t): EntityListItemProps => { + ...(pending.data || []).map((t): TransactionData => { + const notRealTxn = t.source !== 'transaction' return { type: 'transaction', txType: getTransactionType(t.transaction, t.source), @@ -56,10 +78,12 @@ function useTransactionsMain() { timestamp: new Date(t.timestamp).getTime(), sc: new BigNumber(t.inflow).minus(t.outflow), unconfirmed: true, + siascanUrl: notRealTxn ? undefined : siascanUrl, } }), ...(transactions.data || []) - .map((t): EntityListItemProps => { + .map((t): TransactionData => { + const notRealTxn = t.source !== 'transaction' return { type: 'transaction', txType: getTransactionType(t.transaction, t.source), @@ -67,11 +91,12 @@ function useTransactionsMain() { timestamp: new Date(t.timestamp).getTime(), onClick: () => openDialog('transactionDetails', t.ID), sc: new BigNumber(t.inflow).minus(t.outflow), + siascanUrl: notRealTxn ? undefined : siascanUrl, } }) - .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)), + .sort((a, b) => (a['timestamp'] < b['timestamp'] ? 1 : -1)), ] - }, [pending, transactions, openDialog]) + }, [pending, transactions, openDialog, siascanUrl]) const dayPeriods = 30 const start = useMemo(() => { diff --git a/apps/hostd/hooks/useSiascanUrl.ts b/apps/hostd/hooks/useSiascanUrl.ts new file mode 100644 index 000000000..e90e3c150 --- /dev/null +++ b/apps/hostd/hooks/useSiascanUrl.ts @@ -0,0 +1,9 @@ +import { webLinks } from '@siafoundation/design-system' +import { useStateHost } from '@siafoundation/react-hostd' + +export function useSiascanUrl() { + const state = useStateHost() + return state.data?.network === 'Zen Testnet' + ? webLinks.explore.testnetZen + : webLinks.explore.mainnet +} diff --git a/apps/renterd/components/Profile/index.tsx b/apps/renterd/components/Profile/index.tsx index 6efb6b5fe..e048e5b4e 100644 --- a/apps/renterd/components/Profile/index.tsx +++ b/apps/renterd/components/Profile/index.tsx @@ -56,19 +56,20 @@ export function Profile() { - +
+ +
-
+
{state.data?.network}
diff --git a/apps/renterd/contexts/contracts/columns.tsx b/apps/renterd/contexts/contracts/columns.tsx index 8d11b51d9..64cdc4768 100644 --- a/apps/renterd/contexts/contracts/columns.tsx +++ b/apps/renterd/contexts/contracts/columns.tsx @@ -22,6 +22,7 @@ type Context = { startHeight: number endHeight: number } + siascanUrl: string } type ContractsTableColumn = TableColumn< @@ -47,14 +48,19 @@ export const columns: ContractsTableColumn[] = [ id: 'contractId', label: 'contract ID', category: 'general', - render: ({ data: { id, isRenewed, renewedFrom } }) => { + render: ({ + data: { id, isRenewed, renewedFrom }, + context: { siascanUrl }, + }) => { // const { label, color } = getStatus(row) return (
{isRenewed && ( @@ -65,7 +71,9 @@ export const columns: ContractsTableColumn[] = [
@@ -79,13 +87,13 @@ export const columns: ContractsTableColumn[] = [ id: 'hostIp', label: 'host address', category: 'general', - render: ({ data: { hostIp } }) => { + render: ({ data: { hostIp }, context: { siascanUrl } }) => { return ( ) }, @@ -94,8 +102,15 @@ export const columns: ContractsTableColumn[] = [ id: 'hostKey', label: 'host public key', category: 'general', - render: ({ data: { hostKey } }) => { - return + render: ({ data: { hostKey }, context: { siascanUrl } }) => { + return ( + + ) }, }, { diff --git a/apps/renterd/contexts/contracts/index.tsx b/apps/renterd/contexts/contracts/index.tsx index 6173fdbdc..e71f17a9b 100644 --- a/apps/renterd/contexts/contracts/index.tsx +++ b/apps/renterd/contexts/contracts/index.tsx @@ -20,6 +20,7 @@ import { import { columns } from './columns' import { useSiaCentralHosts } from '@siafoundation/react-sia-central' import { useSyncStatus } from '../../hooks/useSyncStatus' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' const defaultLimit = 50 @@ -139,6 +140,17 @@ function useContractsMain() { filters ) + const siascanUrl = useSiascanUrl() + + const cellContext = useMemo( + () => ({ + currentHeight: syncStatus.estimatedBlockHeight, + contractsTimeRange, + siascanUrl, + }), + [syncStatus.estimatedBlockHeight, contractsTimeRange, siascanUrl] + ) + return { dataState, limit, @@ -150,11 +162,8 @@ function useContractsMain() { datasetFilteredCount: datasetFiltered?.length || 0, columns: filteredTableColumns, dataset, + cellContext, datasetPage, - cellContext: { - currentHeight: syncStatus.estimatedBlockHeight, - contractsTimeRange, - }, configurableColumns, enabledColumns, sortableColumns, diff --git a/apps/renterd/contexts/hosts/columns.tsx b/apps/renterd/contexts/hosts/columns.tsx index 0b8f440c0..4da6806fc 100644 --- a/apps/renterd/contexts/hosts/columns.tsx +++ b/apps/renterd/contexts/hosts/columns.tsx @@ -16,7 +16,7 @@ import { CheckboxCheckedFilled16, } from '@siafoundation/react-icons' import { humanBytes, humanNumber } from '@siafoundation/sia-js' -import { HostData, TableColumnId } from './types' +import { HostContext, HostData, TableColumnId } from './types' import { format, formatDistance, formatRelative } from 'date-fns' import { HostContextMenu } from '../../components/Hosts/HostContextMenu' import { useWorkflows } from '@siafoundation/react-core' @@ -29,11 +29,7 @@ import { import BigNumber from 'bignumber.js' import React, { memo } from 'react' -type HostsTableColumn = TableColumn< - TableColumnId, - HostData, - { isAutopilotConfigured: boolean } -> & { +type HostsTableColumn = TableColumn & { fixed?: boolean category: string } @@ -309,12 +305,12 @@ export const columns: HostsTableColumn[] = ( id: 'netAddress', label: 'address', category: 'general', - render: ({ data }) => ( + render: ({ data, context }) => ( ), }, @@ -322,11 +318,12 @@ export const columns: HostsTableColumn[] = ( id: 'publicKey', label: 'public key', category: 'general', - render: ({ data }) => ( + render: ({ data, context }) => ( ), }, diff --git a/apps/renterd/contexts/hosts/index.tsx b/apps/renterd/contexts/hosts/index.tsx index bfc6116f7..bc4f0f67a 100644 --- a/apps/renterd/contexts/hosts/index.tsx +++ b/apps/renterd/contexts/hosts/index.tsx @@ -37,6 +37,7 @@ import { useApp } from '../app' import { useAppSettings } from '@siafoundation/react-core' import { Commands, emptyCommands } from '../../components/Hosts/HostMap/Globe' import { useSiaCentralHosts } from '@siafoundation/react-sia-central' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' const defaultLimit = 50 @@ -240,12 +241,14 @@ function useHostsMain() { autopilot.status === 'on' ? autopilotResponse.error : regularResponse.error const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const siascanUrl = useSiascanUrl() const isAutopilotConfigured = autopilot.state.data?.configured const tableContext = useMemo( () => ({ isAutopilotConfigured, + siascanUrl, }), - [isAutopilotConfigured] + [isAutopilotConfigured, siascanUrl] ) const hostsWithLocation = useMemo( diff --git a/apps/renterd/contexts/hosts/types.tsx b/apps/renterd/contexts/hosts/types.tsx index 99471ef78..2c1e72e2c 100644 --- a/apps/renterd/contexts/hosts/types.tsx +++ b/apps/renterd/contexts/hosts/types.tsx @@ -2,6 +2,8 @@ import { AutopilotHost } from '@siafoundation/react-renterd' import BigNumber from 'bignumber.js' import { ContractData } from '../contracts/types' +export type HostContext = { isAutopilotConfigured: boolean; siascanUrl: string } + export type HostData = { id: string isOnAllowlist: boolean diff --git a/apps/renterd/contexts/transactions/index.tsx b/apps/renterd/contexts/transactions/index.tsx index b25b3fa39..f69e51d57 100644 --- a/apps/renterd/contexts/transactions/index.tsx +++ b/apps/renterd/contexts/transactions/index.tsx @@ -1,8 +1,9 @@ import { - EntityListItemProps, + TxType, daysInMilliseconds, getTransactionType, secondsInMilliseconds, + stripPrefix, useDatasetEmptyState, } from '@siafoundation/design-system' import { @@ -14,10 +15,34 @@ import { createContext, useContext, useMemo } from 'react' import { useDialog } from '../dialog' import BigNumber from 'bignumber.js' import { useRouter } from 'next/router' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' +import { Transaction } from '@siafoundation/react-core' const defaultLimit = 50 const filters = [] +export type TransactionDataPending = { + type: 'transaction' + txType: TxType + siascanUrl: string +} + +export type TransactionDataConfirmed = { + type: 'transaction' + txType: TxType + hash: string + timestamp: number + onClick: () => void + raw: Transaction + inflow: string + outflow: string + unconfirmed: boolean + sc: BigNumber + siascanUrl: string +} + +export type TransactionData = TransactionDataPending | TransactionDataConfirmed + function useTransactionsMain() { const router = useRouter() const limit = Number(router.query.limit || defaultLimit) @@ -45,12 +70,14 @@ function useTransactionsMain() { const { openDialog } = useDialog() - const dataset: EntityListItemProps[] | null = useMemo(() => { + const siascanUrl = useSiascanUrl() + + const dataset: TransactionData[] | null = useMemo(() => { if (!pending.data || !transactions.data) { return null } return [ - ...(pending.data || []).map((t): EntityListItemProps => { + ...(pending.data || []).map((t): TransactionData => { return { type: 'transaction', txType: getTransactionType(t), @@ -59,50 +86,28 @@ function useTransactionsMain() { // onClick: () => openDialog('transactionDetails', t.ID), // sc: totals.sc, unconfirmed: true, + siascanUrl, } }), ...(transactions.data || []) - .map((t): EntityListItemProps => { + .map((t): TransactionData => { return { type: 'transaction', txType: getTransactionType(t.raw), - hash: t.id, + hash: stripPrefix(t.id), timestamp: new Date(t.timestamp).getTime(), - onClick: () => openDialog('transactionDetails', t.id), + onClick: () => openDialog('transactionDetails', stripPrefix(t.id)), + raw: t.raw, + inflow: t.inflow, + outflow: t.outflow, sc: new BigNumber(t.inflow).minus(t.outflow), + siascanUrl, } }) - .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)), + .sort((a, b) => (a['timestamp'] < b['timestamp'] ? 1 : -1)), ] - }, [pending.data, transactions.data, openDialog]) - - // // This now works but the visx chart has an issue where the tooltip does not - // // find the correct nearest datum when the data is not at a consistent - // // interval due to axis scale issues. renterd needs to return clean data - // // like hostd or we need to wait for this issue to be fixed: - // // https://github.com/airbnb/visx/issues/1533 - // // until then renterd will use a line graph which does not have the issue. - // const balances = useMemo( - // () => - // transactions.data - // ?.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)) - // .reduce((acc, t, i) => { - // if (i === 0) { - // return acc.concat({ - // sc: new BigNumber(t.inflow).minus(t.outflow).toNumber(), - // timestamp: new Date(t.timestamp).getTime(), - // }) - // } - // return acc.concat({ - // sc: new BigNumber(acc[i - 1].sc) - // .plus(t.inflow) - // .minus(t.outflow) - // .toNumber(), - // timestamp: new Date(t.timestamp).getTime(), - // }) - // }, []), - // [transactions.data] - // ) + }, [pending.data, transactions.data, openDialog, siascanUrl]) + const error = transactions.error const dataState = useDatasetEmptyState( dataset, @@ -127,6 +132,12 @@ function useTransactionsMain() { n: periods, }, }) + // This now works but the visx chart has an issue where the tooltip does not + // find the correct nearest datum when the data is not at a consistent + // interval due to axis scale issues. renterd needs to return clean data + // like hostd or we need to wait for this issue to be fixed: + // https://github.com/airbnb/visx/issues/1533 + // until then renterd will use a line graph which does not have the issue. const balances = useMemo( () => (metrics.data || []) diff --git a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx index d1bba5086..4f49cceb6 100644 --- a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx +++ b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx @@ -1,43 +1,24 @@ import { useMemo } from 'react' -import { - getTransactionType, - TransactionDetailsDialog, -} from '@siafoundation/design-system' -import { useWalletTransactions } from '@siafoundation/react-renterd' +import { TransactionDetailsDialog } from '@siafoundation/design-system' import { useDialog } from '../contexts/dialog' +import { + TransactionDataConfirmed, + useTransactions, +} from '../contexts/transactions' export function RenterdTransactionDetailsDialog() { const { id, dialog, openDialog, closeDialog } = useDialog() // TODO: add transaction endpoint - const transactions = useWalletTransactions({ - params: {}, - config: { - swr: { - revalidateOnFocus: false, - refreshInterval: 60_000, - }, - }, - disabled: dialog !== 'transactionDetails', - }) + const { dataset } = useTransactions() const transaction = useMemo(() => { - const txn = transactions.data?.find((t) => t.id === id) - if (!txn) { - return null - } - return { - txType: getTransactionType(txn.raw), - inflow: txn.inflow, - outflow: txn.outflow, - timestamp: txn.timestamp, - raw: txn.raw, - } - }, [transactions, id]) + return dataset?.find((t) => t['hash'] === id) + }, [dataset, id]) return ( (val ? openDialog(dialog) : closeDialog())} /> diff --git a/apps/renterd/hooks/useSiascanUrl.tsx b/apps/renterd/hooks/useSiascanUrl.tsx new file mode 100644 index 000000000..40d371f21 --- /dev/null +++ b/apps/renterd/hooks/useSiascanUrl.tsx @@ -0,0 +1,9 @@ +import { webLinks } from '@siafoundation/design-system' +import { useBusState } from '@siafoundation/react-renterd' + +export function useSiascanUrl() { + const network = useBusState() + return network.data?.network === 'Zen Testnet' + ? webLinks.explore.testnetZen + : webLinks.explore.mainnet +} diff --git a/apps/walletd/components/Profile/index.tsx b/apps/walletd/components/Profile/index.tsx index 5faffd516..a9e3c84ec 100644 --- a/apps/walletd/components/Profile/index.tsx +++ b/apps/walletd/components/Profile/index.tsx @@ -28,7 +28,9 @@ export function Profile() { - {network.data.name} +
+ {network.data.name} +
)} {/*
diff --git a/apps/walletd/components/Wallet/index.tsx b/apps/walletd/components/Wallet/index.tsx index 4cad8a431..35337e694 100644 --- a/apps/walletd/components/Wallet/index.tsx +++ b/apps/walletd/components/Wallet/index.tsx @@ -19,6 +19,7 @@ export function Wallet() { dataset, dataState, columns, + cellContext, sortableColumns, sortDirection, sortField, @@ -50,6 +51,7 @@ export function Wallet() { } pageSize={6} data={dataset} + context={cellContext} columns={columns} sortableColumns={sortableColumns} sortDirection={sortDirection} diff --git a/apps/walletd/components/WalletAddresses/index.tsx b/apps/walletd/components/WalletAddresses/index.tsx index 010b58070..17a2ba68e 100644 --- a/apps/walletd/components/WalletAddresses/index.tsx +++ b/apps/walletd/components/WalletAddresses/index.tsx @@ -21,6 +21,7 @@ export function WalletAddresses() { dataset, dataState, columns, + cellContext, sortableColumns, sortDirection, sortField, @@ -75,6 +76,7 @@ export function WalletAddresses() { } pageSize={6} data={dataset} + context={cellContext} columns={columns} sortableColumns={sortableColumns} sortDirection={sortDirection} diff --git a/apps/walletd/contexts/addresses/columns.tsx b/apps/walletd/contexts/addresses/columns.tsx index 5d3fda936..5b57d8f84 100644 --- a/apps/walletd/contexts/addresses/columns.tsx +++ b/apps/walletd/contexts/addresses/columns.tsx @@ -8,9 +8,13 @@ import { } from '@siafoundation/design-system' import { Draggable16 } from '@siafoundation/react-icons' import { AddressContextMenu } from '../../components/WalletAddresses/AddressContextMenu' -import { AddressData, TableColumnId } from './types' +import { AddressData, CellContext, TableColumnId } from './types' -type AddressesTableColumn = TableColumn & { +type AddressesTableColumn = TableColumn< + TableColumnId, + AddressData, + CellContext +> & { fixed?: boolean category?: string } @@ -38,10 +42,15 @@ export const columns: AddressesTableColumn[] = [ label: 'address', category: 'general', fixed: true, - render: ({ data: { address, description } }) => { + render: ({ data: { address, description }, context }) => { return (
- + {description && ( ( + () => ({ + siascanUrl, + }), + [siascanUrl] + ) + return { dataState, error: response.error, datasetCount: datasetFiltered?.length || 0, columns: filteredTableColumns, dataset: datasetFiltered, + cellContext, lastIndex, configurableColumns, enabledColumns, diff --git a/apps/walletd/contexts/addresses/types.ts b/apps/walletd/contexts/addresses/types.ts index 07a6d0286..86a4102f5 100644 --- a/apps/walletd/contexts/addresses/types.ts +++ b/apps/walletd/contexts/addresses/types.ts @@ -1,3 +1,7 @@ +export type CellContext = { + siascanUrl: string +} + export type AddressData = { id: string address: string diff --git a/apps/walletd/contexts/events/columns.tsx b/apps/walletd/contexts/events/columns.tsx index ea4f6479f..c38bcbc0e 100644 --- a/apps/walletd/contexts/events/columns.tsx +++ b/apps/walletd/contexts/events/columns.tsx @@ -7,9 +7,9 @@ import { ValueSf, } from '@siafoundation/design-system' import { humanDate } from '@siafoundation/sia-js' -import { EventData, TableColumnId } from './types' +import { CellContext, EventData, TableColumnId } from './types' -type EventsTableColumn = TableColumn & { +type EventsTableColumn = TableColumn & { fixed?: boolean category?: string } @@ -27,9 +27,15 @@ export const columns: EventsTableColumn[] = [ // label: 'ID', // category: 'general', // fixed: true, - // render: ({ data: { id } }) => { + // render: ({ data: { id }, context }) => { // return ( - // + // // ) // }, // }, @@ -37,12 +43,18 @@ export const columns: EventsTableColumn[] = [ id: 'transactionId', label: 'transaction ID', category: 'general', - render: ({ data: { transactionId } }) => { + render: ({ data: { transactionId }, context }) => { if (!transactionId) { return null } return ( - + ) }, }, @@ -135,11 +147,18 @@ export const columns: EventsTableColumn[] = [ id: 'contractId', label: 'contract ID', category: 'general', - render: ({ data: { contractId } }) => { + render: ({ data: { contractId }, context }) => { if (!contractId) { return null } - return + return ( + + ) }, }, ] diff --git a/apps/walletd/contexts/events/index.tsx b/apps/walletd/contexts/events/index.tsx index 84f3a611a..32daaa765 100644 --- a/apps/walletd/contexts/events/index.tsx +++ b/apps/walletd/contexts/events/index.tsx @@ -16,6 +16,7 @@ import { useMemo, } from 'react' import { + CellContext, EventData, columnsDefaultVisible, defaultSortField, @@ -24,6 +25,7 @@ import { import { columns } from './columns' import { useRouter } from 'next/router' import BigNumber from 'bignumber.js' +import { useSiascanUrl } from '../../hooks/useSiascanUrl' const defaultLimit = 100 @@ -181,12 +183,21 @@ export function useEventsMain() { const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const siascanUrl = useSiascanUrl() + const cellContext = useMemo( + () => ({ + siascanUrl, + }), + [siascanUrl] + ) + return { dataState, error: responseEvents.error, pageCount: dataset?.length || 0, columns: filteredTableColumns, dataset, + cellContext, configurableColumns, enabledColumns, sortableColumns, diff --git a/apps/walletd/contexts/events/types.ts b/apps/walletd/contexts/events/types.ts index 9038cb5dd..b6b617ad5 100644 --- a/apps/walletd/contexts/events/types.ts +++ b/apps/walletd/contexts/events/types.ts @@ -1,5 +1,9 @@ import BigNumber from 'bignumber.js' +export type CellContext = { + siascanUrl: string +} + export type EventData = { id: string transactionId?: string diff --git a/apps/walletd/hooks/useSiascanUrl.ts b/apps/walletd/hooks/useSiascanUrl.ts new file mode 100644 index 000000000..9abfb3ed4 --- /dev/null +++ b/apps/walletd/hooks/useSiascanUrl.ts @@ -0,0 +1,9 @@ +import { webLinks } from '@siafoundation/design-system' +import { useConsensusNetwork } from '@siafoundation/react-walletd' + +export function useSiascanUrl() { + const network = useConsensusNetwork() + return network.data?.name === 'zen' + ? webLinks.explore.testnetZen + : webLinks.explore.mainnet +} diff --git a/libs/design-system/src/app/DaemonProfile/index.tsx b/libs/design-system/src/app/DaemonProfile/index.tsx index fb952d436..86a9cd85b 100644 --- a/libs/design-system/src/app/DaemonProfile/index.tsx +++ b/libs/design-system/src/app/DaemonProfile/index.tsx @@ -34,6 +34,7 @@ export function DaemonProfile({ } > -
+
diff --git a/libs/design-system/src/components/ValueCopyable.tsx b/libs/design-system/src/components/ValueCopyable.tsx index f39cbcdbc..a326c258f 100644 --- a/libs/design-system/src/components/ValueCopyable.tsx +++ b/libs/design-system/src/components/ValueCopyable.tsx @@ -3,11 +3,22 @@ import { Text } from '../core/Text' import { Button } from '../core/Button' import { Link } from '../core/Link' -import { Copy16 } from '@siafoundation/react-icons' +import { CaretDown16, Copy16, Launch16 } from '@siafoundation/react-icons' import { copyToClipboard } from '../lib/clipboard' import { stripPrefix } from '../lib/utils' -import { EntityType, getEntityTypeLabel } from '../lib/entityTypes' +import { + EntityType, + doesEntityHaveSiascanUrl, + getEntityDisplayLength, + getEntitySiascanUrl, + getEntityTypeCopyLabel, +} from '../lib/entityTypes' import { cx } from 'class-variance-authority' +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuLeftSlot, +} from '../core/DropdownMenu' type Props = { value: string @@ -21,6 +32,7 @@ type Props = { maxLength?: number color?: React.ComponentProps['color'] className?: string + siascanUrl?: string } export function ValueCopyable({ @@ -35,9 +47,10 @@ export function ValueCopyable({ weight, color = 'contrast', className, + siascanUrl, }: Props) { - const label = customLabel || getEntityTypeLabel(type) - const maxLength = customMaxLength || (type === 'ip' ? 20 : 12) + const label = customLabel || getEntityTypeCopyLabel(type) + const maxLength = customMaxLength || getEntityDisplayLength(type) const cleanValue = stripPrefix(value) const renderValue = displayValue || cleanValue @@ -71,19 +84,72 @@ export function ValueCopyable({ )}
- +
) } + +export function ValueContextMenu({ + size, + cleanValue, + label, + siascanUrl, + type, +}: { + cleanValue: string + type?: EntityType + label?: string + size?: React.ComponentProps['size'] + siascanUrl?: string +}) { + return ( + + + + } + contentProps={{ align: 'end' }} + > + { + copyToClipboard(cleanValue, label) + }} + onClick={(e) => { + e.stopPropagation() + }} + > + + + + Copy to clipboard + + {siascanUrl && type && doesEntityHaveSiascanUrl(type) && ( + + { + e.stopPropagation() + }} + > + + + + View on Siascan + + + )} + + ) +} diff --git a/libs/design-system/src/components/ValueMenu.tsx b/libs/design-system/src/components/ValueMenu.tsx index d7c9a7dde..d61f000e2 100644 --- a/libs/design-system/src/components/ValueMenu.tsx +++ b/libs/design-system/src/components/ValueMenu.tsx @@ -1,7 +1,7 @@ import { Text } from '../core/Text' import { Link } from '../core/Link' import { stripPrefix } from '../lib/utils' -import { EntityType } from '../lib/entityTypes' +import { EntityType, getEntityDisplayLength } from '../lib/entityTypes' import { cx } from 'class-variance-authority' type Props = { @@ -30,7 +30,7 @@ export function ValueMenu({ menu, className, }: Props) { - const maxLength = customMaxLength || (type === 'ip' ? 20 : 12) + const maxLength = customMaxLength || getEntityDisplayLength(type) const cleanValue = stripPrefix(value) const renderValue = displayValue || cleanValue diff --git a/libs/design-system/src/lib/entityTypes.ts b/libs/design-system/src/lib/entityTypes.ts index e20dc0c1f..9f4dc4230 100644 --- a/libs/design-system/src/lib/entityTypes.ts +++ b/libs/design-system/src/lib/entityTypes.ts @@ -8,6 +8,8 @@ export type EntityType = | 'output' | 'address' | 'ip' + | 'hostIp' + | 'hostPublicKey' | 'contract' export type TxType = @@ -108,12 +110,25 @@ export function getTransactionType( // return undefined } -const entityTypeMap: Record = { +const entityLabels: Record = { transaction: 'transaction', contract: 'contract', block: 'block', output: 'output', address: 'address', + hostIp: 'host', + hostPublicKey: 'host', + ip: 'IP', +} + +const entityCopyLabels: Record = { + transaction: 'transaction ID', + contract: 'contract ID', + block: 'block', + output: 'output ID', + address: 'address', + hostIp: 'host address', + hostPublicKey: 'host public key', ip: 'IP', } @@ -135,9 +150,53 @@ const txTypeMap: Record = { } export function getEntityTypeLabel(type?: EntityType): string | undefined { - return type ? entityTypeMap[type] : undefined + return type ? entityLabels[type] : undefined +} + +export function getEntityTypeCopyLabel(type?: EntityType): string | undefined { + return type ? entityCopyLabels[type] : undefined } export function getTxTypeLabel(type?: TxType): string | undefined { return type ? txTypeMap[type] : undefined } + +export function getEntityDisplayLength(type?: EntityType): number { + const longList: EntityType[] = ['ip', 'hostIp'] + return type && longList.includes(type) ? 20 : 12 +} + +export function doesEntityHaveSiascanUrl(type?: EntityType) { + const includeList: EntityType[] = [ + 'hostIp', + 'hostPublicKey', + 'contract', + 'address', + 'transaction', + 'block', + ] + return type && includeList.includes(type) +} + +export function getEntitySiascanUrl( + baseUrl: string, + type: EntityType, + value: string +) { + switch (type) { + case 'hostIp': + return `${baseUrl}/host/${value}` + case 'hostPublicKey': + return `${baseUrl}/host/${value}` + case 'contract': + return `${baseUrl}/contract/${value}` + case 'transaction': + return `${baseUrl}/tx/${value}` + case 'address': + return `${baseUrl}/address/${value}` + case 'block': + return `${baseUrl}/block/${value}` + default: + return '' + } +}