diff --git a/src/components/GraphChannelList/index.module.scss b/src/components/GraphChannelList/index.module.scss
new file mode 100644
index 000000000..239a50023
--- /dev/null
+++ b/src/components/GraphChannelList/index.module.scss
@@ -0,0 +1,106 @@
+.container {
+ font-size: 0.875rem;
+
+ a {
+ color: var(--primary-color);
+ }
+
+ svg {
+ pointer-events: none;
+ }
+
+ dl {
+ display: flex;
+ gap: 4px;
+ }
+
+ dl,
+ dd,
+ dt {
+ margin: 0;
+ white-space: pre;
+ flex-wrap: wrap;
+ }
+
+ dt {
+ &::after {
+ content: ':';
+ }
+ }
+
+ dd {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .general {
+ dd {
+ .content[data-is-full-width='true'] {
+ & > *:first-child {
+ display: none;
+ }
+
+ @media screen and (width<800px) {
+ & > *:first-child {
+ display: flex;
+ }
+
+ & > *:last-child {
+ display: none;
+ }
+ }
+ }
+
+ .content[data-is-full-width='false'] {
+ & > *:first-child {
+ display: flex;
+ }
+
+ & > *:last-child {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .channel {
+ margin-bottom: 4px;
+ background: #fff;
+ padding: 8px 40px;
+
+ h1 {
+ font-size: 1.2rem;
+ }
+ }
+
+ .nodesContainer {
+ border-radius: 6px;
+ border: 1px solid #ccc;
+ padding: 8px;
+ margin-top: 8px;
+ background: rgb(0 0 0 / 3%);
+ }
+
+ .nodes {
+ display: flex;
+
+ &[data-is-full-width='false'] {
+ flex-direction: column;
+ }
+
+ h3 {
+ font-size: 1rem;
+ }
+
+ gap: 20px;
+
+ .node {
+ flex: 1;
+ }
+
+ @media screen and (width<670px) {
+ flex-direction: column;
+ }
+ }
+}
diff --git a/src/components/GraphChannelList/index.tsx b/src/components/GraphChannelList/index.tsx
new file mode 100644
index 000000000..e789c396b
--- /dev/null
+++ b/src/components/GraphChannelList/index.tsx
@@ -0,0 +1,133 @@
+import { CopyIcon } from '@radix-ui/react-icons'
+import { Tooltip } from 'antd'
+import dayjs from 'dayjs'
+import type { FC } from 'react'
+import { Link } from 'react-router-dom'
+import type { Fiber } from '../../services/ExplorerService/fetcher'
+import { parseNumericAbbr } from '../../utils/chart'
+import { localeNumberString } from '../../utils/number'
+import { shannonToCkb } from '../../utils/util'
+import styles from './index.module.scss'
+
+const TIME_TEMPLATE = 'YYYY/MM/DD hh:mm:ss'
+
+const GraphChannelList: FC<{ list: Fiber.Graph.Channel[]; isFullWidth?: boolean }> = ({ list, isFullWidth = true }) => {
+ return (
+
+ {list.map(channel => {
+ const outPoint = {
+ txHash: channel.channelOutpoint.slice(0, -8),
+ index: parseInt(channel.channelOutpoint.slice(-8), 16),
+ }
+
+ const ckb = shannonToCkb(channel.capacity)
+ const amount = parseNumericAbbr(ckb)
+ return (
+
+
General
+
+
+ - Out Point
+ -
+
+
+
+ {`${outPoint.txHash.slice(0, 6)}...${outPoint.txHash.slice(-6)}#${outPoint.index}`}
+
+
+
+ {`${outPoint.txHash}#${outPoint.index}`}
+
+
+
+
+
+
+
+ - Capacity
+ -
+
+ {`${amount} CKB`}
+
+
+
+
+
+ - Chain Hash
+ -
+
+
+ {`${channel.chainHash.slice(0, 8)}...${channel.chainHash.slice(
+ -8,
+ )}`}
+
+ {channel.chainHash}
+
+
+
+
+
+
+ - Funded at
+ -
+
+ {localeNumberString(channel.fundingTxBlockNumber)}
+
+ (
{dayjs(+channel.lastUpdatedTimestamp).format(TIME_TEMPLATE)}
)
+
+
+
+
+
+
Nodes
+
+
+
First Node
+
+ - Public Key
+ -
+
+ {`${channel.node1.slice(0, 8)}...${channel.node1.slice(-8)}`}
+
+
+
+
+
+ - Fee Rate
+ - {`${localeNumberString(channel.node1ToNode2FeeRate)} shannon/kB`}
+
+
+
+
Second Node
+
+ - Public Key
+ -
+
+ {`${channel.node2.slice(0, 8)}...${channel.node2.slice(-8)}`}
+
+
+
+
+
+ - Fee Rate
+ - {`${localeNumberString(channel.node2ToNode1FeeRate)} shannon/kB`}
+
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+export default GraphChannelList
diff --git a/src/locales/en.json b/src/locales/en.json
index 0c5f2279a..5aaf030a7 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1102,7 +1102,9 @@
},
"graph": {
"node": {
+ "id": "Node ID",
"name": "Name",
+ "alias": "Alias",
"auto_accept_min_ckb_funding_amount": "Auto-accepting Threshold",
"first_seen": "First Seen",
"node_id": "Node ID",
diff --git a/src/pages/Fiber/GraphChannelList/index.module.scss b/src/pages/Fiber/GraphChannelList/index.module.scss
index c5a79afc0..48060a63e 100644
--- a/src/pages/Fiber/GraphChannelList/index.module.scss
+++ b/src/pages/Fiber/GraphChannelList/index.module.scss
@@ -9,18 +9,6 @@
border-radius: 6px;
box-shadow: rgb(0 0 0 / 12%) 0 2px 6px 0;
overflow: hidden;
-
- .list {
- font-size: 0.875rem;
-
- a {
- color: var(--primary-color);
- }
-
- svg {
- pointer-events: none;
- }
- }
}
button {
@@ -42,86 +30,6 @@
margin-bottom: 20px;
}
- .channel {
- margin-bottom: 4px;
- background: #fff;
- padding: 8px 40px;
-
- h1 {
- font-size: 1.2rem;
- }
-
- dl {
- display: flex;
- gap: 4px;
- }
-
- dl,
- dd,
- dt {
- margin: 0;
- white-space: pre;
- flex-wrap: wrap;
- }
-
- dt {
- &::after {
- content: ':';
- }
- }
-
- dd {
- display: flex;
- align-items: center;
- gap: 4px;
- }
-
- .general {
- dd {
- .content {
- & > *:first-child {
- display: none;
- }
-
- @media screen and (width<800px) {
- & > *:first-child {
- display: flex;
- }
-
- & > *:last-child {
- display: none;
- }
- }
- }
- }
- }
-
- .nodesContainer {
- border-radius: 6px;
- border: 1px solid #ccc;
- padding: 8px;
- margin-top: 8px;
- }
-
- .nodes {
- display: flex;
-
- h3 {
- font-size: 1rem;
- }
-
- gap: 20px;
-
- .node {
- flex: 1;
- }
-
- @media screen and (width<670px) {
- flex-direction: column;
- }
- }
- }
-
.pagination {
background: #fff;
padding: 8px 40px;
diff --git a/src/pages/Fiber/GraphChannelList/index.tsx b/src/pages/Fiber/GraphChannelList/index.tsx
index ce1c91fce..1f62bbe2c 100644
--- a/src/pages/Fiber/GraphChannelList/index.tsx
+++ b/src/pages/Fiber/GraphChannelList/index.tsx
@@ -1,22 +1,14 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
-import { Link } from 'react-router-dom'
-import { Tooltip } from 'antd'
-import { CopyIcon } from '@radix-ui/react-icons'
-import dayjs from 'dayjs'
import Content from '../../../components/Content'
import { useSetToast } from '../../../components/Toast'
import { explorerService } from '../../../services/ExplorerService'
-import { shannonToCkb } from '../../../utils/util'
-import { localeNumberString } from '../../../utils/number'
-import { parseNumericAbbr } from '../../../utils/chart'
import styles from './index.module.scss'
import Pagination from '../Pagination'
import { PAGE_SIZE } from '../../../constants/common'
+import GraphChannelListComp from '../../../components/GraphChannelList'
-const TIME_TEMPLATE = 'YYYY/MM/DD hh:mm:ss'
-
-const GraphNodeList = () => {
+const GraphChannelList = () => {
const [t] = useTranslation()
const setToast = useSetToast()
@@ -46,124 +38,7 @@ const GraphNodeList = () => {
CKB Fiber Graph Channels
-
- {list.map(channel => {
- const outPoint = {
- txHash: channel.channelOutpoint.slice(0, -8),
- index: parseInt(channel.channelOutpoint.slice(-8), 16),
- }
-
- const ckb = shannonToCkb(channel.capacity)
- const amount = parseNumericAbbr(ckb)
- return (
-
-
General
-
-
- - Out Point
- -
-
-
-
- {`${outPoint.txHash.slice(0, 6)}...${outPoint.txHash.slice(-6)}#${outPoint.index}`}
-
-
-
- {`${outPoint.txHash}#${outPoint.index}`}
-
-
-
-
-
-
-
- - Capacity
- -
-
- {`${amount} CKB`}
-
-
-
-
-
- - Chain Hash
- -
-
-
- {`${channel.chainHash.slice(0, 8)}...${channel.chainHash.slice(
- -8,
- )}`}
-
- {channel.chainHash}
-
-
-
-
-
-
- - Funded at
- -
-
- {localeNumberString(channel.fundingTxBlockNumber)}
-
- (
{dayjs(+channel.lastUpdatedTimestamp).format(TIME_TEMPLATE)}
)
-
-
-
-
-
-
Nodes
-
-
-
First Node
-
- - Public Key
- -
-
- {`${channel.node1.slice(0, 8)}...${channel.node1.slice(
- -8,
- )}`}
-
-
-
-
-
- - Fee Rate
- - {`${localeNumberString(channel.node1ToNode2FeeRate)} shannon/kB`}
-
-
-
-
Second Node
-
- - Public Key
- -
-
- {`${channel.node2.slice(0, 8)}...${channel.node2.slice(
- -8,
- )}`}
-
-
-
-
-
- - Fee Rate
- - {`${localeNumberString(channel.node2ToNode1FeeRate)} shannon/kB`}
-
-
-
-
-
- )
- })}
-
+
@@ -173,4 +48,4 @@ const GraphNodeList = () => {
)
}
-export default GraphNodeList
+export default GraphChannelList
diff --git a/src/pages/Fiber/GraphNode/index.module.scss b/src/pages/Fiber/GraphNode/index.module.scss
new file mode 100644
index 000000000..ace74b7cd
--- /dev/null
+++ b/src/pages/Fiber/GraphNode/index.module.scss
@@ -0,0 +1,151 @@
+@import '../../../styles/variables.module';
+@import '../../../styles/card.module';
+
+.container {
+ text-wrap: nowrap;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ margin: 24px 120px;
+ font-size: 1rem;
+
+ a {
+ color: var(--primary-color);
+ }
+
+ dl {
+ display: flex;
+
+ dt,
+ dd {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ }
+
+ dt::after {
+ content: ':';
+ margin-right: 4px;
+ }
+ }
+
+ table {
+ width: 100%;
+ text-align: left;
+ cursor: default;
+
+ td,
+ th {
+ padding: 8px;
+ padding-right: 16px;
+
+ &:last-child {
+ text-align: right;
+ }
+ }
+
+ tbody {
+ tr:hover {
+ background: #ccc;
+ }
+ }
+ }
+
+ svg {
+ pointer-events: none;
+ }
+
+ button {
+ display: flex;
+ align-items: center;
+ appearance: none;
+ padding: 0;
+ border: none;
+ background: none;
+ cursor: pointer;
+
+ &:hover {
+ color: var(--primary-color);
+ }
+ }
+
+ .overview {
+ @extend %base-card;
+
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+ .fields {
+ overflow: hidden;
+ }
+ }
+
+ .id,
+ .connectId {
+ overflow: hidden;
+
+ & > span:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex-shrink: 1;
+ }
+ }
+
+ .activities {
+ display: flex;
+ gap: 16px;
+ margin-top: 16px;
+
+ .channels,
+ .transactions {
+ flex: 1;
+ background: #fff;
+ border-radius: 6px;
+ padding: 16px;
+ box-shadow: 0 2px 6px 0 #4d4d4d33;
+
+ h3 {
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ @media screen and (width < 960px) {
+ flex-direction: column;
+ }
+
+ @media screen and (width < 500px) {
+ thead {
+ display: none;
+ }
+
+ tbody {
+ tr {
+ display: flex;
+ flex-direction: column;
+ padding: 16px 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid #ccc;
+ }
+
+ td {
+ text-align: left;
+ padding: 0;
+ }
+ }
+ }
+ }
+ }
+
+ @media screen and (width < $extraLargeBreakPoint) {
+ margin: 24px 20px;
+ }
+
+ @media screen and (width < 1030px) {
+ font-size: 14px;
+ }
+}
diff --git a/src/pages/Fiber/GraphNode/index.tsx b/src/pages/Fiber/GraphNode/index.tsx
new file mode 100644
index 000000000..50c34238a
--- /dev/null
+++ b/src/pages/Fiber/GraphNode/index.tsx
@@ -0,0 +1,182 @@
+import { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useParams } from 'react-router-dom'
+import { useQuery } from '@tanstack/react-query'
+import { CopyIcon, OpenInNewWindowIcon } from '@radix-ui/react-icons'
+import { Tooltip } from 'antd'
+import QRCode from 'qrcode'
+import dayjs from 'dayjs'
+import Content from '../../../components/Content'
+import { explorerService } from '../../../services/ExplorerService'
+import { useSetToast } from '../../../components/Toast'
+import styles from './index.module.scss'
+import Loading from '../../../components/Loading'
+import { shannonToCkb } from '../../../utils/util'
+import { parseNumericAbbr } from '../../../utils/chart'
+import { localeNumberString } from '../../../utils/number'
+
+const TIME_TEMPLATE = 'YYYY/MM/DD hh:mm:ss'
+
+const GraphNode = () => {
+ const [t] = useTranslation()
+ const [addr, setAddr] = useState('')
+ const { id } = useParams<{ id: string }>()
+ const qrRef = useRef
(null)
+
+ const setToast = useSetToast()
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['fiber', 'graph', 'node', id],
+ queryFn: () => {
+ return explorerService.api.getGraphNodeDetail(id)
+ },
+ enabled: !!id,
+ })
+
+ const node = data?.data
+
+ const connectId = addr
+
+ const handleAddrSelect = (e: React.ChangeEvent) => {
+ e.stopPropagation()
+ e.preventDefault()
+ const r = e.currentTarget.value
+ if (r) {
+ setAddr(r)
+ }
+ }
+
+ useEffect(() => {
+ const firstAddr = node?.addresses[0]
+ if (firstAddr) {
+ setAddr(firstAddr)
+ }
+ }, [node, setAddr])
+
+ useEffect(() => {
+ const cvs = qrRef.current
+ if (!cvs || !connectId) return
+ QRCode.toCanvas(
+ cvs,
+ connectId,
+ {
+ margin: 5,
+ errorCorrectionLevel: 'H',
+ width: 144,
+ },
+ err => {
+ if (err) {
+ console.error(err)
+ }
+ },
+ )
+ }, [qrRef, connectId])
+
+ if (isLoading) {
+ return
+ }
+
+ if (!node) {
+ return Fiber Peer Not Found
+ }
+ const channels = node.fiberGraphChannels
+
+ const ckb = shannonToCkb(node.autoAcceptMinCkbFundingAmount)
+ const amount = parseNumericAbbr(ckb)
+
+ const handleCopy = (e: React.SyntheticEvent) => {
+ const elm = e.target
+ if (!(elm instanceof HTMLElement)) return
+ const { copyText } = elm.dataset
+ if (!copyText) return
+ e.stopPropagation()
+ e.preventDefault()
+ navigator?.clipboard.writeText(copyText).then(() => setToast({ message: t('common.copied') }))
+ }
+
+ return (
+
+
+
+
+ {node.alias ? (
+
+ - {t('fiber.graph.alias')}
+ -
+ {node.alias}
+
+
+
+ ) : null}
+
+ - {t('fiber.graph.node.id')}
+ -
+ {node.nodeId}
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+ - {t('fiber.graph.node.first_seen')}
+ - {dayjs(+node.timestamp).format(TIME_TEMPLATE)}
+
+
+ - {t('fiber.graph.node.chain_hash')}
+ - {node.chainHash}
+
+
+ - {t('fiber.graph.node.auto_accept_min_ckb_funding_amount')}
+ -
+
+ {`${amount} CKB`}
+
+
+
+
+ {connectId ? (
+
+
+
+ ) : null}
+
+
+
+
{`${t('fiber.peer.channels')}(${channels.length})`}
+
TODO
+
+
+
Open | Close Transactions
+ Coming soon
+
+
+
+
+ )
+}
+
+export default GraphNode
diff --git a/src/pages/Fiber/GraphNodeList/index.tsx b/src/pages/Fiber/GraphNodeList/index.tsx
index ad22a82f0..7546066d2 100644
--- a/src/pages/Fiber/GraphNodeList/index.tsx
+++ b/src/pages/Fiber/GraphNodeList/index.tsx
@@ -20,7 +20,7 @@ const TIME_TEMPLATE = 'YYYY/MM/DD hh:mm:ss'
const fields = [
{
key: 'alias',
- label: 'name',
+ label: 'alias',
transformer: (v: unknown, i: Fiber.Graph.Node) => {
if (typeof v !== 'string') return v
return (
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 138d71142..a9462c580 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -85,6 +85,7 @@ const FiberPeerList = lazy(() => import('../pages/Fiber/PeerList'))
const FiberPeer = lazy(() => import('../pages/Fiber/Peer'))
const FiberChannel = lazy(() => import('../pages/Fiber/Channel'))
const FiberGraphNodeList = lazy(() => import('../pages/Fiber/GraphNodeList'))
+const FiberGraphNode = lazy(() => import('../pages/Fiber/GraphNode'))
const FiberGraphChannelList = lazy(() => import('../pages/Fiber/GraphChannelList'))
// ======
@@ -364,6 +365,10 @@ const routes: RouteProps[] = [
path: '/fiber/graph/nodes',
component: FiberGraphNodeList,
},
+ {
+ path: '/fiber/graph/node/:id',
+ component: FiberGraphNode,
+ },
{
path: '/fiber/graph/channels',
component: FiberGraphChannelList,