diff --git a/.github/workflows/_external_rust_lints.yml b/.github/workflows/_external_rust_lints.yml index d848357dd01..53fb0f33149 100644 --- a/.github/workflows/_external_rust_lints.yml +++ b/.github/workflows/_external_rust_lints.yml @@ -23,8 +23,7 @@ env: jobs: rustfmt: - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) runs-on: [self-hosted] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 @@ -35,8 +34,7 @@ jobs: cargo +nightly ci-fmt-external cargo-deny: - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) name: cargo-deny (advisories, licenses, bans, ...) runs-on: [self-hosted] steps: @@ -48,8 +46,7 @@ jobs: use-git-cli: true clippy: - if: | - !cancelled() && success('rustfmt', 'cargo-deny') && inputs.isRust + if: (!cancelled() && !failure() && inputs.isRust) needs: - rustfmt - cargo-deny diff --git a/.github/workflows/_external_rust_tests.yml b/.github/workflows/_external_rust_tests.yml index f3512091306..a1056ca3910 100644 --- a/.github/workflows/_external_rust_tests.yml +++ b/.github/workflows/_external_rust_tests.yml @@ -32,8 +32,7 @@ jobs: test: name: Test external crates needs: changes - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) env: # Tests written with #[sim_test] are often flaky if run as #[tokio::test] - this var # causes #[sim_test] to only run under the deterministic `simtest` job, and not the @@ -74,8 +73,7 @@ jobs: check-unused-deps: name: Check Unused Dependencies (${{ matrix.flags }}) - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) strategy: matrix: flags: ["--all-features", "--no-default-features"] diff --git a/.github/workflows/_rust_lints.yml b/.github/workflows/_rust_lints.yml index e7c15960d83..92e5b271fbf 100644 --- a/.github/workflows/_rust_lints.yml +++ b/.github/workflows/_rust_lints.yml @@ -23,8 +23,7 @@ env: jobs: rustfmt: - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) runs-on: [self-hosted] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 @@ -34,8 +33,7 @@ jobs: run: cargo +nightly ci-fmt cargo-deny: - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) name: cargo-deny (advisories, licenses, bans, ...) runs-on: [self-hosted] steps: @@ -47,8 +45,7 @@ jobs: use-git-cli: true clippy: - if: | - !cancelled() && success('rustfmt', 'cargo-deny') && inputs.isRust + if: (!cancelled() && !failure() && inputs.isRust) needs: - rustfmt - cargo-deny diff --git a/.github/workflows/_rust_tests.yml b/.github/workflows/_rust_tests.yml index c83984cdada..ed7b01a8ffa 100644 --- a/.github/workflows/_rust_tests.yml +++ b/.github/workflows/_rust_tests.yml @@ -33,8 +33,7 @@ jobs: test: name: Test rust crates needs: changes - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) env: # Tests written with #[sim_test] are often flaky if run as #[tokio::test] - this var # causes #[sim_test] to only run under the deterministic `simtest` job, and not the @@ -68,8 +67,7 @@ jobs: check-unused-deps: name: Check Unused Dependencies (${{ matrix.flags }}) - if: | - !cancelled() && inputs.isRust + if: (!cancelled() && inputs.isRust) strategy: matrix: flags: ["--all-features", "--no-default-features"] diff --git a/.github/workflows/hierarchy.yml b/.github/workflows/hierarchy.yml index fc12c62ba2a..729cbeb88e3 100644 --- a/.github/workflows/hierarchy.yml +++ b/.github/workflows/hierarchy.yml @@ -54,8 +54,7 @@ jobs: group: license-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true needs: diff - if: | - !cancelled() && needs.diff.outputs.isRust == 'true' + if: (!cancelled() && needs.diff.outputs.isRust == 'true') runs-on: [self-hosted] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 @@ -64,14 +63,12 @@ jobs: docusaurus: needs: diff - if: | - !cancelled() && needs.diff.outputs.isDoc == 'true' && github.event.pull_request.draft == false + if: (!cancelled() && needs.diff.outputs.isDoc == 'true' && !github.event.pull_request.draft) uses: ./.github/workflows/_docusaurus.yml docs-lint: needs: diff - if: | - !cancelled() && needs.diff.outputs.isDoc == 'true' + if: (!cancelled() && needs.diff.outputs.isDoc == 'true') uses: ./.github/workflows/_docs_lint.yml release-notes-description-check: @@ -80,8 +77,7 @@ jobs: group: release-notes-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true needs: diff - if: | - !cancelled() && needs.diff.outputs.isReleaseNotesEligible == 'true' + if: (!cancelled() && needs.diff.outputs.isReleaseNotesEligible == 'true') runs-on: [self-hosted] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 @@ -115,8 +111,7 @@ jobs: - license-check - typos if: | - !cancelled() && - success('dprint-format', 'license-check', 'typos') && + !cancelled() && !failure() && needs.diff.outputs.isRust == 'false' && needs.diff.outputs.isMove == 'true' && github.event.pull_request.draft == false @@ -128,15 +123,14 @@ jobs: - dprint-format - license-check - typos - if: | - !cancelled() && success('dprint-format', 'license-check', 'typos') + if: (!cancelled() && !failure()) uses: ./.github/workflows/_rust.yml with: isRust: ${{ needs.diff.outputs.isRust == 'true' }} secrets: inherit e2e: - if: github.event.pull_request.draft == false + if: (!cancelled() && !failure() && !github.event.pull_request.draft) needs: - diff - dprint-format diff --git a/apps/explorer/src/components/activity/EpochsActivityTable.tsx b/apps/explorer/src/components/activity/EpochsActivityTable.tsx index 3ac88030c7d..13ebd6083f7 100644 --- a/apps/explorer/src/components/activity/EpochsActivityTable.tsx +++ b/apps/explorer/src/components/activity/EpochsActivityTable.tsx @@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; -import { generateTableDataFromEpochsData } from '~/lib/ui'; +import { generateEpochsTableColumns } from '~/lib/ui'; import { numberSuffix } from '~/lib/utils'; const DEFAULT_EPOCHS_LIMIT = 20; @@ -39,7 +39,7 @@ export function EpochsActivityTable({ const { data, isFetching, pagination, isPending, isError } = useCursorPagination(epochMetricsQuery); - const cardData = data ? generateTableDataFromEpochsData(data) : undefined; + const tableColumns = generateEpochsTableColumns(); return (
@@ -48,7 +48,7 @@ export function EpochsActivityTable({ Failed to load Epochs
)} - {isPending || isFetching || !cardData ? ( + {isPending || isFetching || !data?.data ? ( ) : ( { goToFirstPageRef.current(); @@ -58,7 +58,7 @@ export function TransactionsActivityTable({ )}
- {isPending || isFetching || !cardData ? ( + {isPending || isFetching || !data?.data ? ( ) : ( @@ -54,7 +54,7 @@ export function CheckpointsTable({ Failed to load Checkpoints
)} - {isPending || isFetching || !cardData ? ( + {isPending || isFetching || !data?.data ? ( ) : ( > = { - success: 'positive', - error: 'error', -}; +import { Toaster } from '../toaster'; export function Layout(): JSX.Element { const [network, setNetwork] = useNetwork(); @@ -47,25 +40,7 @@ export function Layout(): JSX.Element { - - {(toast) => ( - - {resolveValue(toast.message, toast)} - - )} - + diff --git a/apps/explorer/src/components/object/ObjectFieldsCard.tsx b/apps/explorer/src/components/object/ObjectFieldsCard.tsx index c368f18794a..a83c19f32b5 100644 --- a/apps/explorer/src/components/object/ObjectFieldsCard.tsx +++ b/apps/explorer/src/components/object/ObjectFieldsCard.tsx @@ -156,9 +156,7 @@ export function ObjectFieldsCard({ > diff --git a/apps/explorer/src/components/owned-coins/CoinItem.tsx b/apps/explorer/src/components/owned-coins/CoinItem.tsx index abe73c73824..a79d7cdf12f 100644 --- a/apps/explorer/src/components/owned-coins/CoinItem.tsx +++ b/apps/explorer/src/components/owned-coins/CoinItem.tsx @@ -6,6 +6,7 @@ import { KeyValueInfo } from '@iota/apps-ui-kit'; import { useFormatCoin } from '@iota/core'; import { type CoinStruct } from '@iota/iota-sdk/client'; import { formatAddress } from '@iota/iota-sdk/utils'; +import { ObjectLink } from '../ui'; interface CoinItemProps { coin: CoinStruct; @@ -16,8 +17,9 @@ export default function CoinItem({ coin }: CoinItemProps): JSX.Element { return ( + } fullwidth /> ); diff --git a/apps/explorer/src/components/toaster/Toaster.tsx b/apps/explorer/src/components/toaster/Toaster.tsx new file mode 100644 index 00000000000..14e4fce7d27 --- /dev/null +++ b/apps/explorer/src/components/toaster/Toaster.tsx @@ -0,0 +1,40 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import toast, { Toaster as ToasterLib, type ToastType, resolveValue } from 'react-hot-toast'; +import { Snackbar, SnackbarType } from '@iota/apps-ui-kit'; + +export type ToasterProps = { + bottomNavEnabled?: boolean; +}; + +export function Toaster() { + function getSnackbarType(type: ToastType): SnackbarType { + switch (type) { + case 'success': + return SnackbarType.Default; + case 'error': + return SnackbarType.Error; + case 'loading': + return SnackbarType.Default; + default: + return SnackbarType.Default; + } + } + + return ( + + {(t) => ( +
+ toast.dismiss(t.id)} + text={resolveValue(t.message, t)} + type={getSnackbarType(t.type)} + showClose + duration={t.duration} + /> +
+ )} +
+ ); +} diff --git a/apps/explorer/src/components/toaster/index.ts b/apps/explorer/src/components/toaster/index.ts new file mode 100644 index 00000000000..90ef62a3438 --- /dev/null +++ b/apps/explorer/src/components/toaster/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './Toaster'; diff --git a/apps/explorer/src/components/top-packages/TopPackagesTable.tsx b/apps/explorer/src/components/top-packages/TopPackagesTable.tsx index 05096aecd2d..8aa84ddbf9c 100644 --- a/apps/explorer/src/components/top-packages/TopPackagesTable.tsx +++ b/apps/explorer/src/components/top-packages/TopPackagesTable.tsx @@ -2,62 +2,73 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { TableCellType } from '@iota/apps-ui-kit'; +import { TableCellText, TableCellBase } from '@iota/apps-ui-kit'; import { type MoveCallMetric } from '@iota/iota-sdk/client'; -import { useMemo } from 'react'; +import { type ColumnDef } from '@tanstack/react-table'; -import { createLinkTo, objectToLink, PlaceholderTable, TableCard } from '~/components/ui'; +import { ObjectLink, PlaceholderTable, TableCard } from '~/components/ui'; interface TopPackagesTableProps { data: MoveCallMetric[]; isLoading: boolean; } -export function TopPackagesTable({ data, isLoading }: TopPackagesTableProps) { - const tableData = useMemo( - () => ({ - data: data?.map(([item, count]) => ({ - module: { - type: TableCellType.Link, - label: item.module, - to: createLinkTo(item.package, 'module')({ module: item.module }), - }, - function: { - type: TableCellType.Text, - label: item.function, - }, - package: { - type: TableCellType.Link, - label: item.package, - to: objectToLink({ objectId: item.package }), - }, - count: { - type: TableCellType.Text, - label: Number(count).toLocaleString(), - }, - })), - columns: [ - { - header: 'Package ID', - accessorKey: 'package', - }, - { - header: 'Module', - accessorKey: 'module', - }, - { - header: 'Function', - accessorKey: 'function', - }, - { - header: 'Transactions', - accessorKey: 'count', - }, - ], - }), - [data], - ); +const tableColumns: ColumnDef[] = [ + { + header: 'Module', + id: 'module', + cell({ row: { original: metric } }) { + const item = metric[0]; + return ( + + + {item.module} + + + ); + }, + }, + { + header: 'Function', + id: 'function', + cell({ row: { original: metric } }) { + const item = metric[0]; + return ( + + {item.function} + + ); + }, + }, + { + header: 'Function', + id: 'function', + cell({ row: { original: metric } }) { + const item = metric[0].package; + return ( + + + {item} + + + ); + }, + }, + { + header: 'Count', + id: 'count', + cell({ row: { original: metric } }) { + const item = metric[1]; + return ( + + {item} + + ); + }, + }, +]; +export function TopPackagesTable({ data, isLoading }: TopPackagesTableProps) { if (isLoading) { return ( ; + return ; } diff --git a/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx b/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx index ee08d4967db..543b13f3077 100644 --- a/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx +++ b/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx @@ -5,28 +5,12 @@ import { useIotaClientQuery } from '@iota/dapp-kit'; import { ArrowRight12 } from '@iota/icons'; import { Text } from '@iota/ui'; -import { useMemo } from 'react'; import { Banner, Link, PlaceholderTable, TableCard } from '~/components/ui'; -import { generateValidatorsTableData, type ValidatorTableColumn } from '~/lib/ui'; +import { generateValidatorsTableColumns } from '~/lib/ui'; const NUMBER_OF_VALIDATORS = 10; -const VALIDATOR_COLUMNS: ValidatorTableColumn[] = [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Address', - accessorKey: 'address', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, -]; - type TopValidatorsCardProps = { limit?: number; showIcon?: boolean; @@ -35,23 +19,16 @@ type TopValidatorsCardProps = { export function TopValidatorsCard({ limit, showIcon }: TopValidatorsCardProps): JSX.Element { const { data, isPending, isSuccess, isError } = useIotaClientQuery('getLatestIotaSystemState'); - const tableData = useMemo( - () => - data - ? generateValidatorsTableData({ - validators: [...data.activeValidators].sort(() => 0.5 - Math.random()), - atRiskValidators: [], - validatorEvents: [], - rollingAverageApys: null, - limit, - showValidatorIcon: showIcon, - columns: VALIDATOR_COLUMNS, - }) - : null, - [data, limit, showIcon], - ); + const tableColumns = generateValidatorsTableColumns({ + atRiskValidators: [], + validatorEvents: [], + rollingAverageApys: null, + limit, + showValidatorIcon: showIcon, + includeColumns: ['Name', 'Address', 'Stake'], + }); - if (isError || (!isPending && !tableData?.data.length)) { + if (isError || (!isPending && !data.activeValidators.length)) { return ( Validator data could not be loaded @@ -69,9 +46,9 @@ export function TopValidatorsCard({ limit, showIcon }: TopValidatorsCardProps): /> )} - {isSuccess && tableData && ( + {isSuccess && ( <> - +
diff --git a/apps/explorer/src/components/transaction-blocks-for-address/TransactionBlocksForAddress.tsx b/apps/explorer/src/components/transaction-blocks-for-address/TransactionBlocksForAddress.tsx index 9cec5614b64..790b986dd94 100644 --- a/apps/explorer/src/components/transaction-blocks-for-address/TransactionBlocksForAddress.tsx +++ b/apps/explorer/src/components/transaction-blocks-for-address/TransactionBlocksForAddress.tsx @@ -10,7 +10,6 @@ import { useGetTransactionBlocks, } from '~/hooks/useGetTransactionBlocks'; import { ObjectFilterValue } from '~/lib/enums'; -import { genTableDataFromTxData } from '../transactions/TxCardUtils'; import { ButtonSegment, ButtonSegmentType, @@ -19,6 +18,7 @@ import { SegmentedButtonType, Title, } from '@iota/apps-ui-kit'; +import { generateTransactionsTableColumns } from '~/lib/ui'; type TransactionBlocksForAddressProps = { address: string; @@ -110,10 +110,7 @@ export function TransactionBlocksForAddress({ } as TransactionFilter); const currentPage = currentPageState[filterValue]; - const cardData = - data && data.pages[currentPage] - ? genTableDataFromTxData(data.pages[currentPage].data) - : undefined; + const tableColumns = generateTransactionsTableColumns(); return ( @@ -125,7 +122,10 @@ export function TransactionBlocksForAddress({
- {isPending || isFetching || isFetchingNextPage || !cardData ? ( + {isPending || + isFetching || + isFetchingNextPage || + !data?.pages[currentPage].data ? ( ) : (
- +
)} diff --git a/apps/explorer/src/components/transactions/TransactionsForAddress.tsx b/apps/explorer/src/components/transactions/TransactionsForAddress.tsx index 5095ef24a89..3a50378542d 100644 --- a/apps/explorer/src/components/transactions/TransactionsForAddress.tsx +++ b/apps/explorer/src/components/transactions/TransactionsForAddress.tsx @@ -8,7 +8,7 @@ import { LoadingIndicator, Text } from '@iota/ui'; import { useQuery } from '@tanstack/react-query'; import { Banner, TableCard } from '~/components/ui'; -import { genTableDataFromTxData } from './TxCardUtils'; +import { generateTransactionsTableColumns } from '~/lib/ui'; interface TransactionsForAddressProps { address: string; @@ -44,7 +44,7 @@ export function TransactionsForAddressTable({ ); } - const tableData = genTableDataFromTxData(data); + const tableColumns = generateTransactionsTableColumns(); const hasTxns = data?.length > 0; if (!hasTxns) { @@ -57,7 +57,7 @@ export function TransactionsForAddressTable({ ); } - return ; + return ; } export function TransactionsForAddress({ diff --git a/apps/explorer/src/components/transactions/TxCardUtils.tsx b/apps/explorer/src/components/transactions/TxCardUtils.tsx deleted file mode 100644 index 3264e85b7e1..00000000000 --- a/apps/explorer/src/components/transactions/TxCardUtils.tsx +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { getTotalGasUsed } from '@iota/core'; -import { type IotaClient, type IotaTransactionBlockResponse } from '@iota/iota-sdk/client'; - -import { type TableCellProps, TableCellType } from '@iota/apps-ui-kit'; -import { addressToLink, transactionToLink } from '../ui'; - -interface TransactionData { - date: TableCellProps; - digest: TableCellProps; - txns: TableCellProps; - gas: TableCellProps; - sender: TableCellProps; -} - -interface TableColumn { - header: string; - accessorKey: keyof TransactionData; -} - -// Generate table data from the transaction data - -export function genTableDataFromTxData(results: IotaTransactionBlockResponse[]): { - data: TransactionData[]; - columns: TableColumn[]; -} { - return { - data: results.map((transaction) => { - const sender = transaction.transaction?.data.sender; - - return { - date: { type: TableCellType.Text, label: transaction.timestampMs?.toString() }, - digest: { - type: TableCellType.Link, - label: transaction.digest, - to: transactionToLink({ digest: transaction.digest }), - }, - txns: { - type: TableCellType.Text, - label: - transaction.transaction?.data.transaction.kind === 'ProgrammableTransaction' - ? transaction.transaction.data.transaction.transactions.length.toString() - : '--', - }, - gas: { - type: TableCellType.Text, - label: transaction.effects - ? getTotalGasUsed(transaction.effects)?.toString() - : '0', - }, - sender: sender - ? { - type: TableCellType.Link, - label: sender, - to: addressToLink({ address: sender }), - } - : { - type: TableCellType.Text, - label: '--', - }, - }; - }), - columns: [ - { - header: 'Digest', - accessorKey: 'digest', - }, - { - header: 'Sender', - accessorKey: 'sender', - }, - { - header: 'Txns', - accessorKey: 'txns', - }, - { - header: 'Gas', - accessorKey: 'gas', - }, - { - header: 'Time', - accessorKey: 'date', - }, - ], - }; -} - -const dedupe = (arr: string[]) => Array.from(new Set(arr)); - -export function getDataOnTxDigests( - client: IotaClient, - transactions: string[], -): Promise { - return client - .multiGetTransactionBlocks({ - digests: dedupe(transactions), - options: { - showInput: true, - showEffects: true, - showEvents: true, - }, - }) - .then((transactions) => - // Remove failed transactions - transactions.filter((item) => item), - ); -} diff --git a/apps/explorer/src/components/transactions/index.ts b/apps/explorer/src/components/transactions/index.ts index 968ecec6a40..1a1cc9b6bea 100644 --- a/apps/explorer/src/components/transactions/index.ts +++ b/apps/explorer/src/components/transactions/index.ts @@ -3,4 +3,3 @@ export * from './ProgTxnBlockCard'; export * from './TransactionsForAddress'; -export * from './TxCardUtils'; diff --git a/apps/explorer/src/components/ui/InternalLink.tsx b/apps/explorer/src/components/ui/InternalLink.tsx index 8628e7f8fff..22dfe4d4ee5 100644 --- a/apps/explorer/src/components/ui/InternalLink.tsx +++ b/apps/explorer/src/components/ui/InternalLink.tsx @@ -51,23 +51,3 @@ export const AddressLink = createInternalLink('address', 'address', (addressOrNs export const ObjectLink = createInternalLink('object', 'objectId', formatAddress); export const TransactionLink = createInternalLink('txblock', 'digest', formatDigest); export const ValidatorLink = createInternalLink('validator', 'address', formatAddress); - -// This will ultimately replace createInternalLink. -export function createLinkTo( - base: string, - propName: T, -): (args: { queryStrings?: Record } & Record) => string { - return ({ [propName]: id, queryStrings = {} }) => { - const queryString = new URLSearchParams(queryStrings).toString(); - const queryStringPrefix = queryString ? `?${queryString}` : ''; - - return `/${base}/${encodeURI(id)}${queryStringPrefix}`; - }; -} - -export const transactionToLink = createLinkTo('txblock', 'digest'); -export const checkpointToLink = createLinkTo('checkpoint', 'digest'); -export const epochToLink = createLinkTo('epoch', 'epoch'); -export const addressToLink = createLinkTo('address', 'address'); -export const checkpointSequenceToLink = createLinkTo('checkpoint', 'sequence'); -export const objectToLink = createLinkTo('object', 'objectId'); diff --git a/apps/explorer/src/components/ui/Link.tsx b/apps/explorer/src/components/ui/Link.tsx index 149439f89c2..db60da72395 100644 --- a/apps/explorer/src/components/ui/Link.tsx +++ b/apps/explorer/src/components/ui/Link.tsx @@ -11,7 +11,7 @@ const linkStyles = cva([], { variants: { variant: { text: 'text-body font-semibold text-steel-dark hover:text-steel-darker active:text-steel disabled:text-gray-60', - mono: 'font-mono text-body text-primary-30 hover:text-primary-20 break-all', + mono: 'text-body text-primary-30 hover:text-primary-20', textHeroDark: 'text-pBody font-medium text-hero-dark hover:text-hero-darkest', }, uppercase: { diff --git a/apps/explorer/src/components/ui/PlaceholderTable.tsx b/apps/explorer/src/components/ui/PlaceholderTable.tsx index deb3f0194ec..6c5f3b07007 100644 --- a/apps/explorer/src/components/ui/PlaceholderTable.tsx +++ b/apps/explorer/src/components/ui/PlaceholderTable.tsx @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import { TableCard } from './TableCard'; -import { TableCellType } from '@iota/apps-ui-kit'; +import { TableCellBase, TableCellPlaceholder } from '@iota/apps-ui-kit'; export interface PlaceholderTableProps { rowCount: number; @@ -13,30 +13,30 @@ export interface PlaceholderTableProps { colHeadings: string[]; } +function PlaceholderCell() { + return ( + + + + ); +} + export function PlaceholderTable({ rowCount, rowHeight, colHeadings, }: PlaceholderTableProps): JSX.Element { const rowEntry = useMemo( - () => - Object.fromEntries( - colHeadings.map((header, index) => [ - `a${index}`, - { - type: TableCellType.Placeholder, - }, - ]), - ), + () => Object.fromEntries(colHeadings.map((index) => [`a${index}`, null])), [colHeadings, rowHeight], ); const loadingTable = useMemo( () => ({ data: new Array(rowCount).fill(rowEntry), - columns: colHeadings.map((header, index) => ({ - header: header, - accessorKey: `a${index}`, + columns: colHeadings.map((header) => ({ + header, + cell: PlaceholderCell, })), }), [rowCount, rowEntry, colHeadings], diff --git a/apps/explorer/src/components/ui/TableCard.tsx b/apps/explorer/src/components/ui/TableCard.tsx index 98572fbcd6f..372ecc0cb36 100644 --- a/apps/explorer/src/components/ui/TableCard.tsx +++ b/apps/explorer/src/components/ui/TableCard.tsx @@ -5,16 +5,15 @@ import { Table, TableBody, - TableBodyRow, - TableCell, - type TableCellProps, TableHeader, TableHeaderCell, - TableHeaderRow, + TableRow, + TableActionButton, type TablePaginationOptions, } from '@iota/apps-ui-kit'; import { type ColumnDef, + flexRender, getCoreRowModel, getSortedRowModel, type RowData, @@ -22,8 +21,8 @@ import { useReactTable, } from '@tanstack/react-table'; import clsx from 'clsx'; -import { useState } from 'react'; -import { useNavigateWithQuery } from './LinkWithQuery'; +import { Fragment, useState } from 'react'; +import { Link } from './Link'; export interface TableCardProps { refetching?: boolean; @@ -48,7 +47,6 @@ export function TableCard({ totalLabel, viewAll, }: TableCardProps): JSX.Element { - const navigate = useNavigateWithQuery(); const [sorting, setSorting] = useState(defaultSorting || []); const table = useReactTable({ @@ -72,19 +70,18 @@ export function TableCard({ row.index)} paginationOptions={paginationOptions} - actionLabel={viewAll ? 'View All' : undefined} supportingLabel={totalLabel} - onActionClick={ - viewAll - ? () => { - navigate(viewAll, {}); - } - : undefined + action={ + viewAll ? ( + + + + ) : undefined } > {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map(({ id, column }) => ( ({ isContentCentered={areHeadersCentered} /> ))} - + ))} {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - ()} /> + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} - + ))}
diff --git a/apps/explorer/src/components/validator/ValidatorMeta.tsx b/apps/explorer/src/components/validator/ValidatorMeta.tsx index a426ff23a7b..cc7dd6a0a04 100644 --- a/apps/explorer/src/components/validator/ValidatorMeta.tsx +++ b/apps/explorer/src/components/validator/ValidatorMeta.tsx @@ -6,7 +6,7 @@ import { Badge, BadgeType, KeyValueInfo, Panel } from '@iota/apps-ui-kit'; import { type IotaValidatorSummary } from '@iota/iota-sdk/client'; import toast from 'react-hot-toast'; import { ArrowTopRight } from '@iota/ui-icons'; -import { ImageIcon } from '~/components/ui'; +import { AddressLink, ImageIcon } from '~/components/ui'; type ValidatorMetaProps = { validatorData: IotaValidatorSummary; @@ -60,21 +60,29 @@ export function ValidatorMeta({ validatorData }: ValidatorMetaProps): JSX.Elemen
- + + } + copyText={validatorData.iotaAddress} onCopySuccess={handleOnCopy} /> - +
diff --git a/apps/explorer/src/lib/enums/objectFilterValue.enum.ts b/apps/explorer/src/lib/enums/objectFilterValue.enum.ts index d75e6d5869c..2bbfe28109a 100644 --- a/apps/explorer/src/lib/enums/objectFilterValue.enum.ts +++ b/apps/explorer/src/lib/enums/objectFilterValue.enum.ts @@ -2,6 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export enum ObjectFilterValue { - Input = 'inputObject', - Changed = 'changedObject', + Input = 'InputObject', + Changed = 'ChangedObject', } diff --git a/apps/explorer/src/lib/ui/utils/generateCheckpointsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateCheckpointsTableColumns.tsx new file mode 100644 index 00000000000..571257fe9bf --- /dev/null +++ b/apps/explorer/src/lib/ui/utils/generateCheckpointsTableColumns.tsx @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { TableCellBase, TableCellText } from '@iota/apps-ui-kit'; +import type { Checkpoint } from '@iota/iota-sdk/client'; +import type { ColumnDef } from '@tanstack/react-table'; +import { CheckpointSequenceLink, CheckpointLink } from '~/components'; + +/** + * Generate table columns renderers for the checkpoints data. + */ +export function generateCheckpointsTableColumns(): ColumnDef[] { + return [ + { + header: 'Digest', + accessorKey: 'digest', + cell: ({ getValue }) => { + const digest = getValue(); + return ( + + {digest}} + /> + + ); + }, + }, + { + header: 'Sequence Number', + accessorKey: 'sequenceNumber', + cell: ({ getValue }) => { + const sequenceNumber = getValue(); + return ( + + + + {sequenceNumber} + + + + ); + }, + }, + { + header: 'Transactions', + accessorKey: 'networkTotalTransactions', + cell: ({ getValue }) => { + const networkTotalTransactions = getValue(); + return ( + + {networkTotalTransactions} + + ); + }, + }, + { + header: 'Time', + accessorKey: 'timestampMs', + cell: ({ getValue }) => { + const timestampMs = getValue(); + return ( + + {timestampMs} + + ); + }, + }, + { + header: 'Transaction Block Count', + accessorKey: 'transactions', + cell: ({ getValue }) => { + const transactions = getValue(); + return ( + + {transactions.length} + + ); + }, + }, + ]; +} diff --git a/apps/explorer/src/lib/ui/utils/generateEpochsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateEpochsTableColumns.tsx new file mode 100644 index 00000000000..e7ee623facb --- /dev/null +++ b/apps/explorer/src/lib/ui/utils/generateEpochsTableColumns.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { EpochMetrics } from '@iota/iota-sdk/client'; +import type { ColumnDef } from '@tanstack/react-table'; +import { TableCellBase, TableCellText } from '@iota/apps-ui-kit'; +import { CheckpointSequenceLink, EpochLink } from '~/components'; +import { getEpochStorageFundFlow } from '~/lib/utils'; + +/** + * Generate table columns renderers for the epochs data. + */ +export function generateEpochsTableColumns(): ColumnDef[] { + return [ + { + header: 'Epoch', + accessorKey: 'epoch', + cell: ({ getValue }) => { + const epoch = getValue(); + return ( + + + {epoch} + + + ); + }, + }, + { + header: 'Transaction Blocks', + accessorKey: 'epochTotalTransactions', + cell: ({ getValue }) => { + const epochTotalTransactions = getValue(); + return ( + + {epochTotalTransactions} + + ); + }, + }, + { + header: 'Stake Rewards', + id: 'stakeRewards', + accessorKey: 'endOfEpochInfo.totalStakeRewardsDistributed', + cell: ({ row: { original: epochMetrics } }) => { + const totalStakeRewardsDistributed = + epochMetrics.endOfEpochInfo?.totalStakeRewardsDistributed; + return ( + + {totalStakeRewardsDistributed ?? '0'} + + ); + }, + }, + { + header: 'Checkpoint Set', + accessorKey: 'firstCheckpointId', + cell: ({ getValue }) => { + const firstCheckpointId = getValue(); + return ( + + + + {firstCheckpointId} + + + + ); + }, + }, + { + header: 'Storage Net Inflow', + accessorKey: 'endOfEpochInfo', + cell: ({ getValue }) => { + const endOfEpochInfo = getValue(); + const storageNetInflow = + getEpochStorageFundFlow(endOfEpochInfo).netInflow?.toString() ?? '--'; + return ( + + {storageNetInflow} + + ); + }, + }, + { + header: 'Epoch End', + id: 'epochEndTimestamp', + cell: ({ row: { original: epochMetrics } }) => { + const epochEndTimestamp = epochMetrics.endOfEpochInfo?.epochEndTimestamp; + return ( + + {epochEndTimestamp || '--'} + + ); + }, + }, + ]; +} diff --git a/apps/explorer/src/lib/ui/utils/generateTableDataFromCheckpointsData.tsx b/apps/explorer/src/lib/ui/utils/generateTableDataFromCheckpointsData.tsx deleted file mode 100644 index 2df2bf21c01..00000000000 --- a/apps/explorer/src/lib/ui/utils/generateTableDataFromCheckpointsData.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { type TableCellProps, TableCellType } from '@iota/apps-ui-kit'; -import { type CheckpointPage } from '@iota/iota-sdk/client'; -import { checkpointSequenceToLink, checkpointToLink } from '~/components'; - -interface CheckpointData { - digest: TableCellProps; - time: TableCellProps; - transactions: TableCellProps; - sequenceNumber: TableCellProps; - transactionBlockCount: TableCellProps; -} - -interface TableColumn { - header: string; - accessorKey: keyof CheckpointData; -} - -interface CheckpointTableData { - data: CheckpointData[]; - columns: TableColumn[]; -} - -// Generate table data from the checkpoints data -export function generateTableDataFromCheckpointsData(results: CheckpointPage): CheckpointTableData { - return { - data: - results.data.map((checkpoint) => ({ - digest: { - type: TableCellType.Link, - label: checkpoint.digest, - to: checkpointToLink({ digest: checkpoint.digest }), - }, - time: { type: TableCellType.Text, label: checkpoint.timestampMs }, - transactions: { - type: TableCellType.Text, - label: checkpoint.networkTotalTransactions, - }, - sequenceNumber: { - type: TableCellType.Link, - label: checkpoint.sequenceNumber, - to: checkpointSequenceToLink({ sequence: checkpoint.sequenceNumber }), - }, - transactionBlockCount: { - type: TableCellType.Text, - label: checkpoint.transactions.length.toString(), - }, - })) ?? [], - columns: [ - { - header: 'Digest', - accessorKey: 'digest', - }, - { - header: 'Sequence Number', - accessorKey: 'sequenceNumber', - }, - { - header: 'Transactions', - accessorKey: 'transactions', - }, - { - header: 'Time', - accessorKey: 'time', - }, - { - header: 'Transaction Block Count', - accessorKey: 'transactionBlockCount', - }, - ], - }; -} diff --git a/apps/explorer/src/lib/ui/utils/generateTableDataFromEpochsData.tsx b/apps/explorer/src/lib/ui/utils/generateTableDataFromEpochsData.tsx deleted file mode 100644 index 6fdcd0b54e0..00000000000 --- a/apps/explorer/src/lib/ui/utils/generateTableDataFromEpochsData.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { type EpochMetricsPage } from '@iota/iota-sdk/client'; -import { getEpochStorageFundFlow } from '~/lib/utils'; -import { type TableCellProps, TableCellType } from '@iota/apps-ui-kit'; -import { checkpointSequenceToLink, epochToLink } from '~/components'; - -interface EpochData { - epoch: TableCellProps; - transactions: TableCellProps; - stakeRewards: TableCellProps; - checkpointSet: TableCellProps; - storageNetInflow: TableCellProps; - time: TableCellProps; -} - -interface TableColumn { - header: string; - accessorKey: keyof EpochData; -} - -interface EpochTableData { - data: EpochData[]; - columns: TableColumn[]; -} - -// Generate table data from the epochs data -export function generateTableDataFromEpochsData(results: EpochMetricsPage): EpochTableData { - return { - data: results?.data.map((epoch) => ({ - epoch: { - type: TableCellType.Link, - label: epoch.epoch, - to: epochToLink({ epoch: epoch.epoch }), - }, - transactions: { type: TableCellType.Text, label: epoch.epochTotalTransactions }, - stakeRewards: { - type: TableCellType.Text, - label: epoch.endOfEpochInfo?.totalStakeRewardsDistributed ?? '0', - }, - checkpointSet: { - type: TableCellType.Link, - label: epoch.firstCheckpointId, - to: checkpointSequenceToLink({ sequence: epoch.firstCheckpointId }), - }, - storageNetInflow: { - type: TableCellType.Text, - label: getEpochStorageFundFlow(epoch.endOfEpochInfo).netInflow?.toString() ?? '--', - }, - time: { - type: TableCellType.Text, - label: epoch.endOfEpochInfo?.epochEndTimestamp ?? '--', - }, - })), - columns: [ - { - header: 'Epoch', - accessorKey: 'epoch', - }, - { - header: 'Transaction Blocks', - accessorKey: 'transactions', - }, - { - header: 'Stake Rewards', - accessorKey: 'stakeRewards', - }, - { - header: 'Checkpoint Set', - accessorKey: 'checkpointSet', - }, - { - header: 'Storage Net Inflow', - accessorKey: 'storageNetInflow', - }, - { - header: 'Epoch End', - accessorKey: 'time', - }, - ], - }; -} diff --git a/apps/explorer/src/lib/ui/utils/generateTransactionsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateTransactionsTableColumns.tsx new file mode 100644 index 00000000000..ac57ab6e666 --- /dev/null +++ b/apps/explorer/src/lib/ui/utils/generateTransactionsTableColumns.tsx @@ -0,0 +1,94 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { getTotalGasUsed } from '@iota/core'; +import type { IotaTransactionBlockKind, IotaTransactionBlockResponse } from '@iota/iota-sdk/client'; + +import { TableCellBase, TableCellText } from '@iota/apps-ui-kit'; +import type { ColumnDef } from '@tanstack/react-table'; +import { AddressLink, TransactionLink } from '../../../components/ui'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { getElapsedTime } from '~/pages/epochs/utils'; + +/** + * Generate table columns renderers for the transactions data. + */ +export function generateTransactionsTableColumns(): ColumnDef[] { + return [ + { + header: 'Digest', + accessorKey: 'digest', + cell: ({ getValue }) => { + const digest = getValue(); + return ( + + {formatAddress(digest)}} + /> + + ); + }, + }, + { + header: 'Sender', + accessorKey: 'transaction.data.sender', + cell: ({ getValue }) => { + const address = getValue(); + return ( + + {formatAddress(address)}} + /> + + ); + }, + }, + { + header: 'Txns', + accessorKey: 'transaction.data.transaction', + cell: ({ getValue }) => { + const transaction = getValue(); + const txns = + transaction.kind === 'ProgrammableTransaction' + ? transaction.transactions.length.toString() + : '--'; + return ( + + {txns} + + ); + }, + }, + { + header: 'Gas', + accessorKey: 'effects', + cell: ({ getValue }) => { + const effects = getValue(); + return ( + + + {effects ? getTotalGasUsed(effects)?.toString() : '0'} + + + ); + }, + }, + { + header: 'Time', + accessorKey: 'timestampMs', + cell: ({ getValue }) => { + const timestampMs = getValue(); + return ( + + + {getElapsedTime(Number(timestampMs), Date.now()) || '--'} + + + ); + }, + }, + ]; +} diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx new file mode 100644 index 00000000000..6875e772638 --- /dev/null +++ b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx @@ -0,0 +1,196 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Badge, BadgeType, TableCellBase, TableCellText } from '@iota/apps-ui-kit'; +import type { ColumnDef } from '@tanstack/react-table'; +import { type ApyByValidator, formatPercentageDisplay } from '@iota/core'; + +import { ampli, getValidatorMoveEvent, VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib'; +import { StakeColumn, ValidatorLink, ImageIcon } from '~/components'; +import type { IotaEvent, IotaValidatorSummary } from '@iota/iota-sdk/dist/cjs/client'; + +interface generateValidatorsTableColumnsArgs { + atRiskValidators: [string, string][]; + validatorEvents: IotaEvent[]; + rollingAverageApys: ApyByValidator | null; + limit?: number; + showValidatorIcon?: boolean; + includeColumns?: string[]; +} + +function ValidatorWithImage({ validator }: { validator: IotaValidatorSummary }) { + return ( + + ampli.clickedValidatorRow({ + sourceFlow: 'Epoch details', + validatorAddress: validator.iotaAddress, + validatorName: validator.name, + }) + } + label={ +
+ + {validator.name} +
+ } + /> + ); +} + +export function generateValidatorsTableColumns({ + atRiskValidators = [], + validatorEvents = [], + rollingAverageApys = null, + showValidatorIcon = true, + includeColumns, +}: generateValidatorsTableColumnsArgs): ColumnDef[] { + let columns: ColumnDef[] = [ + { + header: 'Name', + id: 'name', + cell({ row: { original: validator } }) { + return ( + + {showValidatorIcon ? ( + + ) : ( + {validator.name} + )} + + ); + }, + }, + + { + header: 'Stake', + accessorKey: 'stakingPoolIotaBalance', + cell({ getValue }) { + const stakingPoolIotaBalance = getValue(); + return ( + + + + ); + }, + }, + { + header: 'Proposed next Epoch gas price', + accessorKey: 'nextEpochGasPrice', + cell({ getValue }) { + const nextEpochGasPrice = getValue(); + return ( + + + + ); + }, + }, + { + header: 'APY', + accessorKey: 'iotaAddress', + cell({ getValue }) { + const iotaAddress = getValue(); + const { apy, isApyApproxZero } = rollingAverageApys?.[iotaAddress] ?? { + apy: null, + }; + return ( + + + {formatPercentageDisplay(apy, '--', isApyApproxZero)} + + + ); + }, + }, + { + header: 'Comission', + accessorKey: 'commissionRate', + cell({ getValue }) { + return ( + + {`${Number(getValue()) / 100}%`} + + ); + }, + }, + { + header: 'Last Epoch Rewards', + id: 'lastReward', + cell({ row: { original: validator } }) { + const event = getValidatorMoveEvent(validatorEvents, validator.iotaAddress) as { + pool_staking_reward?: string; + }; + const lastReward = event?.pool_staking_reward ?? null; + return ( + + + {lastReward !== null ? ( + + ) : ( + '--' + )} + + + ); + }, + }, + { + header: 'Voting Power', + accessorKey: 'votingPower', + cell({ getValue }) { + const votingPower = getValue(); + return ( + + + {votingPower ? Number(votingPower) / 100 + '%' : '--'} + + + ); + }, + }, + + { + header: 'Status', + id: 'atRisk', + cell({ row: { original: validator } }) { + const atRiskValidator = atRiskValidators.find( + ([address]) => address === validator.iotaAddress, + ); + const isAtRisk = !!atRiskValidator; + const atRisk = isAtRisk + ? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1]) + : null; + + if (atRisk === null) { + return ( + + + + ); + } + + const atRiskText = atRisk > 1 ? `in ${atRisk} epochs` : 'next epoch'; + return ( + + + + ); + }, + }, + ]; + + if (includeColumns) { + columns = columns.filter((col) => + includeColumns.includes(col.header?.toString() as string), + ); + } + + return columns; +} diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx deleted file mode 100644 index 3eabb06a9af..00000000000 --- a/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { - BadgeType, - type TableCellProps, - TableCellType, - TableCellTextColor, -} from '@iota/apps-ui-kit'; -import type { ColumnDef } from '@tanstack/react-table'; -import { type ApyByValidator, formatPercentageDisplay } from '@iota/core'; - -import { ampli, getValidatorMoveEvent, VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib'; -import { Link, StakeColumn, AddressLink, ImageIcon } from '~/components'; -import type { IotaEvent, IotaValidatorSummary } from '@iota/iota-sdk/dist/cjs/client'; - -interface ValidatorTableRow { - name: TableCellProps; - stake: TableCellProps; - apy: TableCellProps; - nextEpochGasPrice: TableCellProps; - commission: TableCellProps; - address: TableCellProps; - lastReward: TableCellProps; - votingPower: TableCellProps; - atRisk: TableCellProps; -} - -interface GenerateValidatorsTableDataArgs { - validators: IotaValidatorSummary[]; - atRiskValidators: [string, string][]; - validatorEvents: IotaEvent[]; - rollingAverageApys: ApyByValidator | null; - limit?: number; - showValidatorIcon?: boolean; - columns?: ColumnDef[]; -} - -const ALL_VALIDATOR_COLUMNS = [ - { - header: '#', - accessorKey: 'number', - }, - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed Next Epoch Gas Price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'Address', - accessorKey: 'address', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Rewards', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -] as const; - -type AccessorKey = (typeof ALL_VALIDATOR_COLUMNS)[number]['accessorKey']; - -export interface ValidatorTableColumn { - header: string; - accessorKey: AccessorKey; -} - -const DEFAULT_COLUMNS: ValidatorTableColumn[] = [ - { - header: '#', - accessorKey: 'number', - }, - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed Next Epoch Gas Price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Rewards', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -]; - -function generateValidatorAtRisk(atRisk: number | null): TableCellProps { - if (atRisk === null) { - return { - type: TableCellType.Badge, - badgeType: BadgeType.PrimarySoft, - label: 'Active', - }; - } - - const atRiskText = atRisk > 1 ? `in ${atRisk} epochs` : 'next epoch'; - return { - type: TableCellType.Badge, - badgeType: BadgeType.Neutral, - label: `At Risk ${atRiskText}`, - }; -} - -function ValidatorName({ - address, - name, - imageUrl, -}: { - address: string; - name: string; - imageUrl: string; -}) { - return ( - - ampli.clickedValidatorRow({ - sourceFlow: 'Epoch details', - validatorAddress: address, - validatorName: name, - }) - } - > -
- - {name} -
- - ); -} - -function ValidatorAddress({ - address, - name, - limit, -}: { - address: string; - name: string; - limit?: number; -}) { - return ( -
- - ampli.clickedValidatorRow({ - sourceFlow: 'Top validators - validator address', - validatorAddress: address, - validatorName: name, - }) - } - /> -
- ); -} - -export function generateValidatorsTableData({ - validators, - limit, - atRiskValidators = [], - validatorEvents = [], - rollingAverageApys = null, - showValidatorIcon = true, - columns = DEFAULT_COLUMNS, -}: GenerateValidatorsTableDataArgs): { - data: ValidatorTableRow[]; - columns: ColumnDef[]; -} { - return { - data: validators.map((validator, i) => { - const validatorName = validator.name; - const totalStake = validator.stakingPoolIotaBalance; - - const event = getValidatorMoveEvent(validatorEvents, validator.iotaAddress) as { - pool_staking_reward?: string; - }; - - const atRiskValidator = atRiskValidators.find( - ([address]) => address === validator.iotaAddress, - ); - const isAtRisk = !!atRiskValidator; - const lastReward = event?.pool_staking_reward ?? null; - const { apy, isApyApproxZero } = rollingAverageApys?.[validator.iotaAddress] ?? { - apy: null, - }; - - return { - number: { - type: TableCellType.Text, - label: `${i + 1}`, - textColor: TableCellTextColor.Dark, - }, - name: showValidatorIcon - ? { - type: TableCellType.Children, - children: ( - - ), - } - : { - type: TableCellType.Text, - label: validatorName, - textColor: TableCellTextColor.Dark, - }, - stake: { - type: TableCellType.Children, - children: , - noWrap: true, - }, - nextEpochGasPrice: { - type: TableCellType.Children, - children: , - noWrap: true, - }, - apy: { - type: TableCellType.Text, - label: formatPercentageDisplay(apy, '--', isApyApproxZero), - }, - commission: { - type: TableCellType.Text, - label: `${Number(validator.commissionRate) / 100}%`, - }, - lastReward: - lastReward !== null - ? { - type: TableCellType.Children, - children: , - noWrap: true, - } - : { - type: TableCellType.Text, - label: '--', - }, - votingPower: { - type: TableCellType.Text, - label: validator.votingPower ? Number(validator.votingPower) / 100 + '%' : '--', - }, - atRisk: generateValidatorAtRisk( - isAtRisk ? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1]) : null, - ), - address: { - type: TableCellType.Children, - children: ( - - ), - }, - }; - }), - columns: columns, - }; -} diff --git a/apps/explorer/src/lib/ui/utils/index.ts b/apps/explorer/src/lib/ui/utils/index.ts index 217017f0324..ba75c5c3c1a 100644 --- a/apps/explorer/src/lib/ui/utils/index.ts +++ b/apps/explorer/src/lib/ui/utils/index.ts @@ -1,7 +1,8 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export * from './generateTableDataFromCheckpointsData'; -export * from './generateTableDataFromEpochsData'; -export * from './generateValidatorsTableData'; +export * from './generateCheckpointsTableColumns'; +export * from './generateEpochsTableColumns'; +export * from './generateValidatorsTableColumns'; +export * from './generateTransactionsTableColumns'; export * from './objectField'; diff --git a/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx b/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx index 86e4b7d452d..029eb40c032 100644 --- a/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx +++ b/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx @@ -4,12 +4,12 @@ import { useState } from 'react'; -import { genTableDataFromTxData } from '~/components'; import { Pagination, PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; import { DEFAULT_TRANSACTIONS_LIMIT, useGetTransactionBlocks, } from '~/hooks/useGetTransactionBlocks'; +import { generateTransactionsTableColumns } from '~/lib/ui'; export function CheckpointTransactionBlocks({ id }: { id: string }): JSX.Element { const [limit, setLimit] = useState(DEFAULT_TRANSACTIONS_LIMIT); @@ -22,11 +22,11 @@ export function CheckpointTransactionBlocks({ id }: { id: string }): JSX.Element const { data, isFetching, pagination, isPending } = useCursorPagination(transactions); - const cardData = data ? genTableDataFromTxData(data.data) : undefined; + const tableColumns = generateTransactionsTableColumns(); return (
- {isPending || isFetching || !cardData ? ( + {isPending || isFetching || !data?.data ? ( ) : (
- +
)}
diff --git a/apps/explorer/src/pages/epochs/EpochDetail.tsx b/apps/explorer/src/pages/epochs/EpochDetail.tsx index e64155ccd9c..35c9553a7ad 100644 --- a/apps/explorer/src/pages/epochs/EpochDetail.tsx +++ b/apps/explorer/src/pages/epochs/EpochDetail.tsx @@ -19,49 +19,11 @@ import { Banner, TableCard } from '~/components/ui'; import { useEnhancedRpcClient } from '~/hooks/useEnhancedRpc'; import { EpochStats, EpochStatsGrid } from './stats/EpochStats'; import { ValidatorStatus } from './stats/ValidatorStatus'; +import { generateValidatorsTableColumns } from '~/lib/ui/utils/generateValidatorsTableColumns'; import cx from 'clsx'; import { TokenStats } from './stats/TokenStats'; import { EpochTopStats } from './stats/EpochTopStats'; import { getEpochStorageFundFlow } from '~/lib/utils'; -import { - generateValidatorsTableData, - type ValidatorTableColumn, -} from '~/lib/ui/utils/generateValidatorsTableData'; - -export const VALIDATOR_COLUMNS: ValidatorTableColumn[] = [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed next Epoch gas price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Reward', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -]; enum EpochTabs { Checkpoints = 'checkpoints', @@ -89,17 +51,25 @@ export default function EpochDetail() { [systemState, epochData], ); - const validatorsTable = useMemo(() => { + const tableColumns = useMemo(() => { if (!epochData?.validators || epochData.validators.length === 0) return null; // todo: enrich this historical validator data when we have // at-risk / pending validators for historical epochs - return generateValidatorsTableData({ - validators: [...epochData.validators].sort(() => 0.5 - Math.random()), + return generateValidatorsTableColumns({ atRiskValidators: [], validatorEvents: [], rollingAverageApys: null, - columns: VALIDATOR_COLUMNS, showValidatorIcon: true, + includeColumns: [ + 'Name', + 'Stake', + 'Proposed next Epoch gas price', + 'APY', + 'Comission', + 'Last Epoch Rewards', + 'Voting Power', + 'Status', + ], }); }, [epochData]); @@ -116,6 +86,8 @@ export default function EpochDetail() { /> ); + const tableData = [...epochData.validators].sort(() => 0.5 - Math.random()); + const { fundInflow, fundOutflow, netInflow } = getEpochStorageFundFlow( epochData.endOfEpochInfo, ); @@ -214,11 +186,8 @@ export default function EpochDetail() { initialLimit={20} /> ) : null} - {activeTabId === EpochTabs.Validators && validatorsTable ? ( - + {activeTabId === EpochTabs.Validators && tableData && tableColumns ? ( + ) : null}
diff --git a/apps/explorer/src/pages/epochs/utils.ts b/apps/explorer/src/pages/epochs/utils.ts index 62cd6c37ff2..93eba55d178 100644 --- a/apps/explorer/src/pages/epochs/utils.ts +++ b/apps/explorer/src/pages/epochs/utils.ts @@ -57,25 +57,31 @@ export function useEpochProgress(suffix: string = 'left'): EpochProgress { }; } -export function getElapsedTime(start: number, end: number) { +export function getElapsedTime(start: number, end: number): string { const diff = end - start; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); - const displayMinutes = minutes - hours * 60; - const displaySeconds = seconds - minutes * 60; + const displayHours = hours % 24; + const displayMinutes = minutes % 60; + const displaySeconds = seconds % 60; - const renderTime = []; + const renderTime: string[] = []; - if (hours > 0) { - renderTime.push(`${hours}h`); + if (days > 0) { + renderTime.push(`${days}d`); + } + if (displayHours > 0) { + renderTime.push(`${displayHours}h`); } if (displayMinutes > 0) { renderTime.push(`${displayMinutes}m`); } - if (displaySeconds > 0) { + if (displaySeconds > 0 || renderTime.length === 0) { + // Ensure at least seconds are shown renderTime.push(`${displaySeconds}s`); } diff --git a/apps/explorer/src/pages/object-result/views/ObjectView.tsx b/apps/explorer/src/pages/object-result/views/ObjectView.tsx index af34eb65f7d..3938b3d01f9 100644 --- a/apps/explorer/src/pages/object-result/views/ObjectView.tsx +++ b/apps/explorer/src/pages/object-result/views/ObjectView.tsx @@ -14,8 +14,8 @@ import { } from '@iota/iota-sdk/utils'; import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; -import { ObjectVideoImage } from '~/components/ui'; +import { type PropsWithChildren, type ReactNode, useEffect, useState } from 'react'; +import { AddressLink, Link, ObjectLink, ObjectVideoImage, TransactionLink } from '~/components/ui'; import { useResolveVideo } from '~/hooks/useResolveVideo'; import { extractName, @@ -82,8 +82,7 @@ function ObjectIdCard({ objectId }: ObjectIdCardProps): JSX.Element { return ( {formatAddress(objectId)}} /> ); } @@ -118,8 +117,11 @@ function TypeCard({ objectType }: TypeCardCardProps): JSX.Element { return ( + {normalizedStructTag} + + } tooltipText={objectType} tooltipPosition={TooltipPosition.Right} /> @@ -142,8 +144,7 @@ function LastTxBlockCard({ digest }: LastTxBlockCardProps): JSX.Element { return ( {formatAddress(digest)}} /> ); } @@ -164,24 +165,28 @@ function OwnerCard({ objOwner }: OwnerCardProps): JSX.Element | null { : formatAddress(objOwner.AddressOwner); } - function getOwnerLink(objOwner: ObjectOwner): string | null { - if (objOwner !== 'Immutable' && !('Shared' in objOwner)) { - return 'ObjectOwner' in objOwner - ? `/object/${objOwner.ObjectOwner}` - : `/address/${objOwner.AddressOwner}`; - } - return null; - } - return ( {getOwner(objOwner)}} /> ); } +function OwnerLink({ + children, + objOwner, +}: PropsWithChildren<{ objOwner: ObjectOwner }>): ReactNode { + if (objOwner !== 'Immutable' && !('Shared' in objOwner)) { + if ('ObjectOwner' in objOwner) { + return {children}; + } else { + return {children}; + } + } + return null; +} + interface StorageRebateCardProps { storageRebate: string; } @@ -298,13 +303,15 @@ export function ObjectView({ data }: ObjectViewProps): JSX.Element {
{display && display.link && ( - + {display.link}} + /> )} {display && display.project_url && ( {display.project_url}} /> )}
diff --git a/apps/explorer/src/pages/object-result/views/PkgView.tsx b/apps/explorer/src/pages/object-result/views/PkgView.tsx index 4cfe6a418a2..9f0780064eb 100644 --- a/apps/explorer/src/pages/object-result/views/PkgView.tsx +++ b/apps/explorer/src/pages/object-result/views/PkgView.tsx @@ -7,7 +7,13 @@ import { LoadingIndicator } from '@iota/ui'; import { useState } from 'react'; import { type Direction } from 'react-resizable-panels'; -import { ErrorBoundary, PkgModulesWrapper, TransactionBlocksForAddress } from '~/components'; +import { + AddressLink, + ErrorBoundary, + ObjectLink, + PkgModulesWrapper, + TransactionBlocksForAddress, +} from '~/components'; import { getOwnerStr, trimStdLibPrefix } from '~/lib/utils'; import { type DataType } from '../ObjectResultType'; @@ -77,15 +83,19 @@ function PkgView({ data }: PkgViewProps): JSX.Element {
} /> - + + {viewedData?.publisherAddress && ( + } /> )}
diff --git a/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx b/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx index c4b8af67ef1..ae72d0876c2 100644 --- a/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx +++ b/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx @@ -46,36 +46,38 @@ enum ItemLabel { const DEFAULT_ITEMS_TO_SHOW = 5; function Item({ label, packageId, moduleName, typeName }: ItemProps): JSX.Element | null { - function getValueData() { - switch (label) { - case ItemLabel.Package: - return { - text: packageId ? formatAddress(packageId) : '', - link: packageId ? `/object/${packageId}` : undefined, - }; - case ItemLabel.Module: - return { - text: moduleName || '', - link: - packageId && moduleName - ? `/object/${packageId}?module=${moduleName}` - : undefined, - }; - case ItemLabel.Type: - return { - text: typeName || '', - link: undefined, - }; - default: - return { - text: '', - link: undefined, - }; - } + switch (label) { + case ItemLabel.Package: + return ( + + } + fullwidth + /> + ); + case ItemLabel.Module: + return ( + + } + fullwidth + /> + ); + case ItemLabel.Type: + return ; + default: + return ; } - const { text: valueText, link: valueLink } = getValueData(); - - return ; } interface ObjectDetailPanelProps { diff --git a/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx b/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx index 8261528fd21..1dc9a856e9d 100644 --- a/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx +++ b/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx @@ -2,8 +2,9 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { DisplayStats } from '@iota/apps-ui-kit'; +import { DisplayStats, truncate } from '@iota/apps-ui-kit'; import { formatDate } from '@iota/core'; +import { AddressLink, CheckpointSequenceLink, EpochLink } from '~/components'; interface TransactionDetailsProps { sender?: string; @@ -23,23 +24,23 @@ export function TransactionDetails({ {sender && ( {truncate(sender)}} /> )} {checkpoint && ( + {Number(checkpoint).toLocaleString()} + + } /> )} {executedEpoch && ( {executedEpoch}} /> )} diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index b3874e85d74..52255bff5c2 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -20,7 +20,7 @@ import { TableCard, TableHeader, } from '~/components'; -import { generateValidatorsTableData } from '~/lib/ui/utils/generateValidatorsTableData'; +import { generateValidatorsTableColumns } from '~/lib/ui'; function ValidatorPageResult(): JSX.Element { const { data, isPending, isSuccess, isError } = useIotaClientQuery('getLatestIotaSystemState'); @@ -72,10 +72,11 @@ function ValidatorPageResult(): JSX.Element { return totalRewards; }, [validatorEvents]); - const validatorsTable = useMemo(() => { + const tableData = data ? [...data.activeValidators].sort(() => 0.5 - Math.random()) : []; + + const tableColumns = useMemo(() => { if (!data || !validatorEvents) return null; - return generateValidatorsTableData({ - validators: [...data.activeValidators].sort(() => 0.5 - Math.random()), + return generateValidatorsTableColumns({ atRiskValidators: data.atRiskValidators, validatorEvents, rollingAverageApys: validatorsApy || null, @@ -151,10 +152,10 @@ function ValidatorPageResult(): JSX.Element { /> )} - {isSuccess && validatorsTable?.data && ( + {isSuccess && tableData && tableColumns && ( )} diff --git a/apps/ui-kit/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx b/apps/ui-kit/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx index a31bcb170c6..c484febd047 100644 --- a/apps/ui-kit/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx +++ b/apps/ui-kit/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { ReactNode } from 'react'; import cx from 'classnames'; import { Copy, Info } from '@iota/ui-icons'; import { ValueSize } from './keyValue.enums'; @@ -14,13 +14,9 @@ interface KeyValueProps { */ keyText: string; /** - * The value text of the KeyValue. + * The value of the KeyValue. */ - valueText: string; - /** - * The value link of the KeyValue. - */ - valueLink?: string; + value: ReactNode; /** * The tooltip position. */ @@ -53,10 +49,6 @@ interface KeyValueProps { * The onCopyError event of the KeyValue (optional). */ onCopyError?: (e: unknown, text: string) => void; - /** - * Has copy icon (optional). - */ - isCopyable?: boolean; /** * Full width KeyValue (optional). */ @@ -65,17 +57,15 @@ interface KeyValueProps { export function KeyValueInfo({ keyText, - valueText, + value, tooltipPosition, tooltipText, supportingLabel, - valueLink, size = ValueSize.Small, isTruncated = false, - copyText = valueText, + copyText, onCopySuccess, onCopyError, - isCopyable, fullwidth, }: KeyValueProps): React.JSX.Element { async function handleCopyClick(event: React.MouseEvent) { @@ -83,12 +73,14 @@ export function KeyValueInfo({ return; } - try { - await navigator.clipboard.writeText(copyText); - onCopySuccess?.(event, copyText); - } catch (error) { - console.error('Failed to copy:', error); - onCopyError?.(error, copyText); + if (copyText) { + try { + await navigator.clipboard.writeText(copyText); + onCopySuccess?.(event, copyText); + } catch (error) { + console.error('Failed to copy:', error); + onCopyError?.(error, copyText); + } } } @@ -116,42 +108,27 @@ export function KeyValueInfo({ truncate: isTruncated, })} > - {valueLink ? ( - - {valueText} - - ) : ( - <> - - {valueText} - - {supportingLabel && ( - - {supportingLabel} - + + {value} + + {supportingLabel && ( + + > + {supportingLabel} + )}
- {isCopyable && ( + {copyText && ( diff --git a/apps/ui-kit/src/lib/components/atoms/snackbar/Snackbar.tsx b/apps/ui-kit/src/lib/components/atoms/snackbar/Snackbar.tsx index d9e7b1ad69a..173ae161c1b 100644 --- a/apps/ui-kit/src/lib/components/atoms/snackbar/Snackbar.tsx +++ b/apps/ui-kit/src/lib/components/atoms/snackbar/Snackbar.tsx @@ -57,7 +57,7 @@ export function Snackbar({ BACKGROUND_COLOR[type], )} > -
{text}
+
{text}
{showClose && (
{onOptionsClick && ( -
) : ( -
{showSelected && ( -