feat: add ICANN-list tab

This commit is contained in:
Maël Gangloff
2025-09-14 17:59:26 +02:00
parent 7c10f4bd3c
commit ac264d9a13
7 changed files with 416 additions and 142 deletions

View File

@@ -4,7 +4,7 @@ import TextPage from './pages/TextPage'
import DomainSearchPage from './pages/search/DomainSearchPage'
import EntitySearchPage from './pages/search/EntitySearchPage'
import NameserverSearchPage from './pages/search/NameserverSearchPage'
import TldPage from './pages/search/TldPage'
import TldPage from './pages/infrastructure/TldPage'
import StatisticsPage from './pages/StatisticsPage'
import WatchlistPage from './pages/tracking/WatchlistPage'
import UserPage from './pages/UserPage'
@@ -18,6 +18,7 @@ import {Sider} from './components/Sider'
import {jt, t} from 'ttag'
import {BugOutlined, InfoCircleOutlined, MergeOutlined} from '@ant-design/icons'
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
import IcannRegistrarPage from "./pages/infrastructure/IcannRegistrarPage";
const PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
@@ -94,8 +95,9 @@ export default function App(): React.ReactElement {
<Route path='/search/domain' element={<DomainSearchPage/>}/>
<Route path='/search/domain/:query' element={<DomainSearchPage/>}/>
<Route path='/search/entity' element={<EntitySearchPage/>}/>
<Route path='/search/nameserver' element={<NameserverSearchPage/>}/>
<Route path='/search/tld' element={<TldPage/>}/>
<Route path='/infrastructure/tld' element={<TldPage/>}/>
<Route path='/infrastructure/icann' element={<IcannRegistrarPage/>}/>
<Route path='/tracking/watchlist' element={<WatchlistPage/>}/>
<Route path='/tracking/domains' element={<TrackedDomainPage/>}/>

View File

@@ -4,14 +4,17 @@ import {
AimOutlined,
ApiOutlined,
BankOutlined,
ClusterOutlined,
CompassOutlined,
FileSearchOutlined,
HomeOutlined,
LineChartOutlined,
LoginOutlined,
LogoutOutlined,
SafetyOutlined,
SearchOutlined,
TableOutlined,
TeamOutlined,
UserOutlined
} from '@ant-design/icons'
import {Menu} from 'antd'
@@ -44,22 +47,14 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
onClick: () => navigate('/search/domain')
},
{
key: '/search/tld',
icon: <BankOutlined/>,
label: t`TLD`,
title: t`TLD list`,
key: '/search/entity',
icon: <TeamOutlined/>,
label: t`Entity`,
title: t`Entity Finder`,
disabled: !isAuthenticated,
onClick: () => navigate('/search/tld')
}
onClick: () => navigate('/search/entity')
},
/*
{
key: 'entity-finder',
icon: <TeamOutlined/>,
label: t`Entity`,
title: t`Entity Finder`,
disabled: true,
onClick: () => navigate('/search/entity')
},
{
key: 'ns-finder',
icon: <CloudServerOutlined/>,
@@ -71,6 +66,31 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
*/
]
},
{
key: 'infrastructure',
icon: <ClusterOutlined/>,
label: t`Infrastructure`,
title: t`Infrastructure`,
disabled: !isAuthenticated,
children: [
{
key: '/infrastructure/tld',
icon: <BankOutlined/>,
label: t`TLD`,
title: t`TLD list`,
disabled: !isAuthenticated,
onClick: () => navigate('/infrastructure/tld')
},
{
key: '/infrastructure/icann',
icon: <SafetyOutlined/>,
label: t`ICANN list`,
title: t`ICANN list`,
disabled: !isAuthenticated,
onClick: () => navigate('/infrastructure/icann')
}
]
},
{
key: 'tracking',
label: t`Tracking`,
@@ -99,6 +119,21 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
}
]
},
/*
{
key: 'tools',
label: t`Tools`,
icon: <ToolOutlined/>,
children: [
{
key: '/tools/idn',
icon: <FieldStringOutlined/>,
label: t`IDN Conversion`,
onClick: () => navigate('/')
}
]
},
*/
{
key: '/stats',
icon: <LineChartOutlined/>,
@@ -131,7 +166,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
}
return <Menu
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
defaultOpenKeys={['search', 'info', 'tracking', 'doc', 'infrastructure']}
selectedKeys={[location.pathname.includes('/search/domain') ? '/search/domain' : location.pathname]}
mode='inline'
theme='dark'

View File

@@ -0,0 +1,134 @@
import React, {useEffect, useState} from 'react'
import {Card, Divider, Table, Typography} from 'antd'
import type {IcannAccreditation} from '../../utils/api'
import {t} from 'ttag'
import type {ColumnType} from 'antd/es/table'
import {CheckCircleOutlined, SettingOutlined, CloseCircleOutlined} from "@ant-design/icons"
import {getIcannAccreditations} from "../../utils/api/icann-accreditations";
const {Text, Paragraph} = Typography
interface FiltersType {
'icannAccreditation.status': 'Accredited' | 'Reserved' | 'Terminated',
}
function RegistrarListTable(filters: FiltersType) {
interface TableRow {
key: string
handle: number
name: string
}
const [dataTable, setDataTable] = useState<TableRow[]>([])
const [total, setTotal] = useState(0)
const fetchData = (params: FiltersType & { page: number, itemsPerPage: number }) => {
getIcannAccreditations(params).then((data) => {
setTotal(data['hydra:totalItems'])
setDataTable(data['hydra:member'].map((accreditation: IcannAccreditation) => ({
key: accreditation.handle,
handle: parseInt(accreditation.handle),
name: accreditation.icannAccreditation.registrarName
})
).sort((a, b) => a.handle - b.handle))
})
}
useEffect(() => {
fetchData({...filters, page: 1, itemsPerPage: 30})
}, [])
let columns: Array<ColumnType<TableRow>> = [
{
title: t`ID`,
dataIndex: 'handle',
width: '10vh',
align: 'center'
},
{
title: t`Registrar`,
dataIndex: 'name'
}
]
return (
<Table
columns={columns}
dataSource={dataTable}
pagination={{
total,
hideOnSinglePage: true,
defaultPageSize: 30,
onChange: (page, itemsPerPage) => {
fetchData({...filters, page, itemsPerPage})
}
}}
scroll={{y: '50vh'}}
/>
)
}
export default function IcannRegistrarPage() {
const [activeTabKey, setActiveTabKey] = useState<string>('Accredited')
const contentList: Record<string, React.ReactNode> = {
Accredited: <>
<Text>{t`An accredited number means that ICANN's contract with the registrar is ongoing.`}</Text>
<Divider/>
<RegistrarListTable {...{'icannAccreditation.status': 'Accredited'}} />
</>,
Reserved: <>
<Text>{t`A reserved number can be used by TLD registries for specific operations.`}</Text>
<Divider/>
<RegistrarListTable {...{'icannAccreditation.status': 'Reserved'}} />
</>,
Terminated: <>
<Text>{t`A terminated number means that ICANN's contract with the registrar has been terminated.`}</Text>
<Divider/>
<RegistrarListTable {...{'icannAccreditation.status': 'Terminated'}} />
</>
}
return (
<>
<Paragraph>
{t`This page lists ICANN-accredited registrars.`}
</Paragraph>
<Paragraph>
{t`The list is officially published and maintained by the Internet Assigned Numbers Authority (IANA), the organization responsible for managing the Internet's unique identifiers (including numbers, IP addresses, and domain name extensions).`}
</Paragraph>
<Divider/>
<Card
style={{width: '100%'}}
tabProps={{
size: 'middle',
}}
tabList={[
{
key: 'Accredited',
label: t`Accredited`,
icon: <CheckCircleOutlined/>
},
{
key: 'Reserved',
label: t`Reserved`,
icon: <SettingOutlined/>
},
{
key: 'Terminated',
label: t`Terminated`,
icon: <CloseCircleOutlined />
}
]}
activeTabKey={activeTabKey}
key={activeTabKey}
onTabChange={(k: string) => setActiveTabKey(k)}
>
{contentList[activeTabKey]}
</Card>
</>
)
}

View File

@@ -10,6 +10,7 @@ import punycode from 'punycode/punycode'
import {getCountryCode} from '../../utils/functions/getCountryCode'
import {tldToEmoji} from '../../utils/functions/tldToEmoji'
import {BankOutlined, FlagOutlined, GlobalOutlined, TrademarkOutlined} from "@ant-design/icons"
import {Link} from "react-router-dom";
const {Text, Paragraph} = Typography
@@ -38,7 +39,7 @@ function TldTable(filters: FiltersType) {
setDataTable(data['hydra:member'].map((tld: Tld) => {
const rowData = {
key: tld.tld,
TLD: <Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text>
TLD: <Link to={'/search/domain/' + tld.tld}><Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text></Link>
}
const type = filters.type
let countryName

View File

@@ -0,0 +1,14 @@
import type {IcannAccreditation, Tld} from './index'
import {request} from './index'
interface IcannAccreditationList {
'hydra:totalItems': number
'hydra:member': IcannAccreditation[]
}
export async function getIcannAccreditations(params: object): Promise<IcannAccreditationList> {
return (await request<IcannAccreditationList>({
url: 'entities/icann-accreditations',
params
})).data
}

View File

@@ -124,6 +124,16 @@ export interface TrackedDomains {
'hydra:member': Domain[]
}
export interface IcannAccreditation {
handle: string
icannAccreditation: {
registrarName: string
status: string
date?: string
updated?: string
}
}
export async function request<T = object, R = AxiosResponse<T>, D = object>(config: AxiosRequestConfig): Promise<R> {
const axiosConfig: AxiosRequestConfig = {
...config,