mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
Merge branch 'develop' into feat/openprovider-provider
This commit is contained in:
104
assets/App.tsx
104
assets/App.tsx
@@ -1,4 +1,4 @@
|
||||
import {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, Typography} from 'antd'
|
||||
import {Button, ConfigProvider, Drawer, Flex, Layout, theme, Typography} from 'antd'
|
||||
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
|
||||
import TextPage from './pages/TextPage'
|
||||
import DomainSearchPage from './pages/search/DomainSearchPage'
|
||||
@@ -7,7 +7,8 @@ import TldPage from './pages/infrastructure/TldPage'
|
||||
import StatisticsPage from './pages/StatisticsPage'
|
||||
import WatchlistPage from './pages/tracking/WatchlistPage'
|
||||
import UserPage from './pages/UserPage'
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import type {PropsWithChildren} from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {getUser} from './utils/api'
|
||||
import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
|
||||
import ConnectorPage from './pages/tracking/ConnectorPage'
|
||||
@@ -15,21 +16,56 @@ import NotFoundPage from './pages/NotFoundPage'
|
||||
import useBreakpoint from './hooks/useBreakpoint'
|
||||
import {Sider} from './components/Sider'
|
||||
import {jt, t} from 'ttag'
|
||||
import {BugOutlined, InfoCircleOutlined, MergeOutlined} from '@ant-design/icons'
|
||||
import {MenuOutlined} 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'
|
||||
|
||||
const ProjectLink = <Typography.Link target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
|
||||
const LicenseLink = <Typography.Link target='_blank' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link>
|
||||
const ProjectLink = <Typography.Link key="projectLink" target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
|
||||
const LicenseLink = <Typography.Link key="licenceLink" target='_blank' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link>
|
||||
|
||||
function SiderWrapper(props: PropsWithChildren<{sidebarCollapsed: boolean, setSidebarCollapsed: (collapsed: boolean) => void}>): React.ReactElement {
|
||||
const {sidebarCollapsed, setSidebarCollapsed, children} = props
|
||||
const sm = useBreakpoint('sm')
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
if (sm) {
|
||||
setSidebarCollapsed(false)
|
||||
}
|
||||
}, [location])
|
||||
|
||||
if (sm) {
|
||||
return <Drawer
|
||||
placement="left"
|
||||
open={sidebarCollapsed}
|
||||
onClose={() => setSidebarCollapsed(false)}
|
||||
closeIcon={null}
|
||||
styles={{body: {padding: 0, height: '100%', background: '#001529'}}}
|
||||
width='200px'>
|
||||
{children}
|
||||
</Drawer>
|
||||
} else {
|
||||
return <Layout.Sider
|
||||
collapsible
|
||||
breakpoint='sm'
|
||||
width={220}
|
||||
trigger={null}
|
||||
collapsed={sidebarCollapsed && sm}
|
||||
{...(sm ? {collapsedWidth: 0} : {})}>
|
||||
{children}
|
||||
</Layout.Sider>
|
||||
}
|
||||
}
|
||||
|
||||
export default function App(): React.ReactElement {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
|
||||
const authenticated = useCallback((authenticated: boolean) => {
|
||||
@@ -75,15 +111,20 @@ export default function App(): React.ReactElement {
|
||||
>
|
||||
<AuthenticatedContext.Provider value={contextValue}>
|
||||
<Layout hasSider style={{minHeight: '100vh'}}>
|
||||
{/* Ant will use a break-off tab to toggle the collapse of the sider when collapseWidth = 0 */}
|
||||
<Layout.Sider collapsible breakpoint='sm' width={220} {...(sm ? {collapsedWidth: 0} : {})}>
|
||||
<SiderWrapper sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed}>
|
||||
<Sider isAuthenticated={isAuthenticated}/>
|
||||
</Layout.Sider>
|
||||
</SiderWrapper>
|
||||
<Layout>
|
||||
<Layout.Header style={{padding: 0}}/>
|
||||
<Layout.Header style={{padding: 0}}>
|
||||
{sm &&
|
||||
<Button type="text" style={{marginLeft: 8}} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}>
|
||||
<MenuOutlined />
|
||||
</Button>
|
||||
}
|
||||
</Layout.Header>
|
||||
<Layout.Content style={sm ? {margin: '24px 0'} : {margin: '24px 16px 0'}}>
|
||||
<div style={{
|
||||
padding: 24,
|
||||
padding: sm ? 8 : 24,
|
||||
minHeight: 360
|
||||
}}
|
||||
>
|
||||
@@ -116,41 +157,40 @@ export default function App(): React.ReactElement {
|
||||
</div>
|
||||
</Layout.Content>
|
||||
<Layout.Footer style={{textAlign: 'center'}}>
|
||||
<Space size='middle' wrap align='center'>
|
||||
<Flex gap='middle' wrap justify='center'>
|
||||
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
|
||||
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
||||
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href='https://github.com/maelgangloff/domain-watchdog/wiki'
|
||||
href='https://domainwatchdog.eu'
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
>{t`Documentation`}
|
||||
<Button type='text'>
|
||||
{t`Documentation`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
</Space>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href={PROJECT_LINK}
|
||||
>
|
||||
<Button type='text'>
|
||||
{t`Source code`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href={PROJECT_LINK + '/issues'}
|
||||
>
|
||||
<Button type='text'>
|
||||
{t`Submit an issue`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
</Flex>
|
||||
<Typography.Paragraph style={{marginTop: '1em'}}>
|
||||
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
|
||||
</Typography.Paragraph>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
<FloatButton.Group
|
||||
trigger='hover'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
insetInlineEnd: (100 - 40) / 2,
|
||||
bottom: 100 - 40 / 2
|
||||
}}
|
||||
icon={<InfoCircleOutlined/>}
|
||||
>
|
||||
<Tooltip title={t`Official git repository`} placement='left'>
|
||||
<FloatButton icon={<MergeOutlined/>} target='_blank' href={PROJECT_LINK}/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t`Submit an issue`} placement='left'>
|
||||
<FloatButton icon={<BugOutlined/>} target='_blank' href={PROJECT_LINK + '/issues'}/>
|
||||
</Tooltip>
|
||||
</FloatButton.Group>
|
||||
</Layout>
|
||||
</AuthenticatedContext.Provider>
|
||||
</ConfigProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Button, Form, Input, message, Space} from 'antd'
|
||||
import {Button, Flex, Form, Input, message} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React, {useContext, useEffect} from 'react'
|
||||
import React, {useContext, useEffect, useState} from 'react'
|
||||
import {getUser, login} from '../utils/api'
|
||||
import {AuthenticatedContext} from '../pages/LoginPage'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
@@ -16,6 +16,7 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
const navigate = useNavigate()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const {setIsAuthenticated} = useContext(AuthenticatedContext)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(() => {
|
||||
@@ -25,12 +26,15 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
}, [])
|
||||
|
||||
const onFinish = (data: FieldType) => {
|
||||
setLoading(true)
|
||||
|
||||
login(data.username, data.password).then(() => {
|
||||
setIsAuthenticated(true)
|
||||
navigate('/home')
|
||||
}).catch((e) => {
|
||||
setIsAuthenticated(false)
|
||||
showErrorAPI(e, messageApi)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
return (
|
||||
@@ -43,6 +47,7 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
style={{maxWidth: 600}}
|
||||
onFinish={onFinish}
|
||||
autoComplete='off'
|
||||
disabled={loading}
|
||||
>
|
||||
<Form.Item
|
||||
label={t`Email address`}
|
||||
@@ -60,18 +65,15 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
|
||||
<Space>
|
||||
<Form.Item wrapperCol={{offset: 8, span: 16}}>
|
||||
<Flex wrap justify="center" gap="middle">
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t`Submit`}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{ssoLogin && <Form.Item wrapperCol={{offset: 8, span: 16}}>
|
||||
{ssoLogin &&
|
||||
<Button type='dashed' htmlType='button' href='/login/oauth'>
|
||||
{t`Log in with SSO`}
|
||||
</Button>
|
||||
</Form.Item>}
|
||||
</Space>
|
||||
</Button>}
|
||||
</Flex>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -22,16 +22,16 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
|
||||
<>
|
||||
<Divider/>
|
||||
{connectors.map(connector => {
|
||||
const createdAt = <Typography.Text strong>
|
||||
const createdAt = <Typography.Text strong key={"createdAt"}>
|
||||
{new Date(connector.createdAt).toLocaleString()}
|
||||
</Typography.Text>
|
||||
const {watchlistCount} = connector
|
||||
const connectorName = Object.keys(ConnectorProvider).find(p => ConnectorProvider[p as keyof typeof ConnectorProvider] === connector.provider)
|
||||
|
||||
return <>
|
||||
{contextHolder}
|
||||
<Card
|
||||
hoverable title={<Space>
|
||||
return <Card
|
||||
hoverable
|
||||
key={connector.id}
|
||||
title={<Space>
|
||||
{t`Connector ${connectorName}`}<Typography.Text code>{connector.id}</Typography.Text>
|
||||
</Space>}
|
||||
size='small'
|
||||
@@ -45,6 +45,7 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
|
||||
><DeleteFilled style={{color: token.colorError}}/>
|
||||
</Popconfirm>}
|
||||
>
|
||||
{contextHolder}
|
||||
<Typography.Paragraph>{jt`Creation date: ${createdAt}`}</Typography.Paragraph>
|
||||
<Typography.Paragraph>{t`Used in: ${watchlistCount} Watchlist`}</Typography.Paragraph>
|
||||
<Card.Meta description={
|
||||
@@ -58,7 +59,6 @@ The creation date corresponds to the date on which you consented to the creation
|
||||
</>
|
||||
}/>
|
||||
</Card>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -42,7 +42,6 @@ export function CreateWatchlistButton({onUpdateWatchlist, connectors}: {
|
||||
paddingBottom: 80
|
||||
}
|
||||
}}
|
||||
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
|
||||
>
|
||||
<WatchlistForm
|
||||
form={form}
|
||||
|
||||
@@ -22,11 +22,13 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import {DomainToTag} from '../../../utils/functions/DomainToTag'
|
||||
import {isDomainLocked} from "../../../utils/functions/isDomainLocked"
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint"
|
||||
|
||||
export function TrackedDomainTable() {
|
||||
const REDEMPTION_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}
|
||||
key="redeptionNotice"
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
|
||||
</Tooltip>
|
||||
@@ -35,6 +37,7 @@ export function TrackedDomainTable() {
|
||||
const PENDING_DELETE_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is pending deletion and will soon become available for registration again`}
|
||||
key="pendingDeleteNotice"
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
|
||||
</Tooltip>
|
||||
@@ -53,6 +56,7 @@ export function TrackedDomainTable() {
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState<number>()
|
||||
const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([])
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
@@ -220,6 +224,7 @@ export function TrackedDomainTable() {
|
||||
text: <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
key={s}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>,
|
||||
@@ -268,7 +273,8 @@ export function TrackedDomainTable() {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
scroll={{y: '50vh'}}
|
||||
scroll={sm ? {} : {y: '50vh'}}
|
||||
size={sm ? 'small' : 'large'}
|
||||
/>
|
||||
</Skeleton>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Button, Drawer, Form, Typography} from 'antd'
|
||||
import {Drawer, Form, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from './WatchlistForm'
|
||||
import React, {useState} from 'react'
|
||||
@@ -53,7 +53,6 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
paddingBottom: 80
|
||||
}
|
||||
}}
|
||||
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
|
||||
>
|
||||
<WatchlistForm
|
||||
form={form}
|
||||
|
||||
101
assets/components/tracking/watchlist/WatchlistSelectionModal.tsx
Normal file
101
assets/components/tracking/watchlist/WatchlistSelectionModal.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, {useEffect, useState} from "react"
|
||||
import type {ModalProps} from "antd"
|
||||
import {Tag, Tooltip} from "antd"
|
||||
import {Flex, Modal, Select, Typography} from "antd"
|
||||
import type {Domain, Watchlist} from "../../../utils/api"
|
||||
import {getWatchlists} from "../../../utils/api"
|
||||
import {t} from 'ttag'
|
||||
import {DomainToTag} from "../../../utils/functions/DomainToTag"
|
||||
import {EllipsisOutlined} from '@ant-design/icons'
|
||||
|
||||
const MAX_DOMAIN_TAGS = 25
|
||||
|
||||
function WatchlistOption({watchlist}: {watchlist: Watchlist}) {
|
||||
let domains = watchlist.domains
|
||||
let rest: Domain[]|undefined = undefined
|
||||
|
||||
if (domains.length > MAX_DOMAIN_TAGS) {
|
||||
rest = domains.slice(MAX_DOMAIN_TAGS)
|
||||
domains = domains.slice(0, MAX_DOMAIN_TAGS)
|
||||
}
|
||||
|
||||
return <Flex vertical>
|
||||
<Typography.Text strong>{watchlist.name}</Typography.Text>
|
||||
<Flex wrap gap='4px'>
|
||||
{domains.map(d => <DomainToTag link={false} domain={d} key={d.ldhName} />)}
|
||||
{rest
|
||||
&& <Tooltip title={rest.map(d => <DomainToTag link={false} domain={d} key={d.ldhName} />)}>
|
||||
<Tag icon={<EllipsisOutlined/>} color='processing'>
|
||||
{t`${rest.length} more`}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
interface WatchlistSelectionModalProps {
|
||||
onFinish: (watchlist: Watchlist) => Promise<void>|void
|
||||
description?: string
|
||||
open?: boolean
|
||||
modalProps?: Partial<ModalProps>
|
||||
}
|
||||
|
||||
export default function WatchlistSelectionModal(props: WatchlistSelectionModalProps) {
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[] | undefined>()
|
||||
const [selectedWatchlist, setSelectedWatchlist] = useState<Watchlist | undefined>()
|
||||
const [validationLoading, setValidationLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open && !watchlists) {
|
||||
getWatchlists().then(list => setWatchlists(list["hydra:member"]))
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
const onFinish = () => {
|
||||
const promise = props.onFinish(selectedWatchlist as Watchlist)
|
||||
|
||||
if (promise) {
|
||||
setValidationLoading(true)
|
||||
promise.finally(() => {
|
||||
setSelectedWatchlist(undefined)
|
||||
setValidationLoading(false)
|
||||
})
|
||||
} else {
|
||||
setSelectedWatchlist(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return <Modal
|
||||
open={props.open}
|
||||
onOk={onFinish}
|
||||
okButtonProps={{
|
||||
disabled: !selectedWatchlist,
|
||||
loading: validationLoading,
|
||||
}}
|
||||
{...props.modalProps ?? {}}
|
||||
>
|
||||
<Flex vertical>
|
||||
<Typography.Paragraph>
|
||||
{
|
||||
props.description
|
||||
|| t`Select one of your available Watchlists`
|
||||
}
|
||||
</Typography.Paragraph>
|
||||
<Select
|
||||
placeholder={t`Watchlist`}
|
||||
style={{width: '100%'}}
|
||||
onChange={(_, option) => setSelectedWatchlist(option as Watchlist)}
|
||||
options={watchlists}
|
||||
value={selectedWatchlist?.token}
|
||||
fieldNames={{
|
||||
label: 'name',
|
||||
value: 'token',
|
||||
}}
|
||||
loading={!watchlists}
|
||||
status={selectedWatchlist ? '' : 'error'}
|
||||
optionRender={(watchlist) => <WatchlistOption watchlist={watchlist.data}/>}
|
||||
/>
|
||||
</Flex>
|
||||
</Modal>
|
||||
}
|
||||
@@ -10,4 +10,8 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Noto Color Emoji", sans-serif;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {LoginForm} from '../components/LoginForm'
|
||||
import type { InstanceConfig} from '../utils/api'
|
||||
import {getConfiguration} from '../utils/api'
|
||||
import {RegisterForm} from '../components/RegisterForm'
|
||||
import useBreakpoint from "../hooks/useBreakpoint"
|
||||
|
||||
export const AuthenticatedContext = createContext<
|
||||
{
|
||||
@@ -22,6 +23,7 @@ export const AuthenticatedContext = createContext<
|
||||
export default function LoginPage() {
|
||||
const [wantRegister, setWantRegister] = useState<boolean>(false)
|
||||
const [configuration, setConfiguration] = useState<InstanceConfig>()
|
||||
const md = useBreakpoint('md')
|
||||
|
||||
const toggleWantRegister = () => {
|
||||
setWantRegister(!wantRegister)
|
||||
@@ -31,24 +33,32 @@ export default function LoginPage() {
|
||||
getConfiguration().then(setConfiguration)
|
||||
}, [])
|
||||
|
||||
const grid = [
|
||||
<Card.Grid key="form" style={{width: md ? '100%' : '50%', textAlign: 'center'}} hoverable={false}>
|
||||
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
|
||||
{
|
||||
configuration?.registerEnabled &&
|
||||
<Button
|
||||
type='link'
|
||||
block
|
||||
style={{marginTop: '1em'}}
|
||||
onClick={toggleWantRegister}
|
||||
>{wantRegister ? t`Log in` : t`Create an account`}
|
||||
</Button>
|
||||
}
|
||||
</Card.Grid>,
|
||||
<Card.Grid key="ads" style={{width: md ? '100%' : '50%'}} hoverable={false}>
|
||||
<TextPage resource='ads.md'/>
|
||||
</Card.Grid>
|
||||
]
|
||||
|
||||
if (md) {
|
||||
grid.reverse()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
|
||||
<Card.Grid style={{width: '50%', textAlign: 'center'}} hoverable={false}>
|
||||
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
|
||||
{
|
||||
configuration?.registerEnabled &&
|
||||
<Button
|
||||
type='link'
|
||||
block
|
||||
style={{marginTop: '1em'}}
|
||||
onClick={toggleWantRegister}
|
||||
>{wantRegister ? t`Log in` : t`Create an account`}
|
||||
</Button>
|
||||
}
|
||||
</Card.Grid>
|
||||
<Card.Grid style={{width: '50%'}} hoverable={false}>
|
||||
<TextPage resource='ads.md'/>
|
||||
</Card.Grid>
|
||||
{grid}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import type { Statistics} from '../utils/api'
|
||||
import {getStatistics} from '../utils/api'
|
||||
import {Card, Col, Divider, Row, Statistic, Tooltip} from 'antd'
|
||||
import {Card, Col, Divider, Flex, Row, Statistic, Tooltip} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {
|
||||
AimOutlined,
|
||||
@@ -18,10 +18,8 @@ export default function StatisticsPage() {
|
||||
getStatistics().then(setStats)
|
||||
}, [])
|
||||
|
||||
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
|
||||
|
||||
const successRate = stats !== undefined
|
||||
? (totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
|
||||
? (stats?.domainPurchased === 0 ? undefined : ((stats?.domainPurchased ?? 0) - (stats?.domainPurchaseFailed ?? 0)) / stats?.domainPurchased)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
@@ -82,7 +80,7 @@ export default function StatisticsPage() {
|
||||
loading={stats === undefined}
|
||||
title={t`Purchased domain names`}
|
||||
prefix={<FieldTimeOutlined/>}
|
||||
value={stats?.domainPurchased}
|
||||
value={(stats?.domainPurchased??0) - (stats?.domainPurchaseFailed??0)}
|
||||
valueStyle={{color: 'green'}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -95,7 +93,7 @@ export default function StatisticsPage() {
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={t`Success rate`}
|
||||
value={successRate === undefined ? '-' : successRate * 100}
|
||||
value={successRate === undefined ? '-' : (successRate * 100).toFixed(2)}
|
||||
suffix='%'
|
||||
valueStyle={{color: successRate === undefined ? 'grey' : successRate >= 0.5 ? 'darkgreen' : 'orange'}}
|
||||
/>
|
||||
@@ -104,20 +102,19 @@ export default function StatisticsPage() {
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider/>
|
||||
<Row gutter={16} justify='center' align='middle'>
|
||||
<Flex gap={16} wrap justify='center' align='middle'>
|
||||
{stats?.domainCount
|
||||
.sort((a, b) => b.domain - a.domain)
|
||||
.map(({domain, tld}) => <Col key={tld} span={4}>
|
||||
<Card bordered={false}>
|
||||
.map(({domain, tld}) =>
|
||||
<Card key={tld} bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={tld ? tld : t`TLD`}
|
||||
value={domain}
|
||||
valueStyle={{color: 'darkorange'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</Card>)}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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"
|
||||
import useBreakpoint from "../../hooks/useBreakpoint"
|
||||
|
||||
const {Text, Paragraph} = Typography
|
||||
|
||||
@@ -19,6 +20,7 @@ function RegistrarListTable(filters: FiltersType) {
|
||||
name: string
|
||||
}
|
||||
|
||||
const sm = useBreakpoint('sm')
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
@@ -63,14 +65,15 @@ function RegistrarListTable(filters: FiltersType) {
|
||||
fetchData({...filters, page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
|
||||
scroll={{y: '50vh'}}
|
||||
scroll={sm ? {} : {y: '50vh'}}
|
||||
size={sm ? 'small' : 'large'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function IcannRegistrarPage() {
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>('Accredited')
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const contentList: Record<string, React.ReactNode> = {
|
||||
Accredited: <>
|
||||
@@ -125,6 +128,7 @@ export default function IcannRegistrarPage() {
|
||||
activeTabKey={activeTabKey}
|
||||
key={activeTabKey}
|
||||
onTabChange={(k: string) => setActiveTabKey(k)}
|
||||
size={sm ? 'small' : 'default'}
|
||||
>
|
||||
{contentList[activeTabKey]}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
import useBreakpoint from "../../hooks/useBreakpoint"
|
||||
|
||||
const {Text, Paragraph} = Typography
|
||||
|
||||
@@ -30,6 +31,7 @@ function TldTable(filters: FiltersType) {
|
||||
Country?: string
|
||||
}
|
||||
|
||||
const sm = useBreakpoint('sm')
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
@@ -110,14 +112,15 @@ function TldTable(filters: FiltersType) {
|
||||
fetchData({...filters, page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
|
||||
scroll={{y: '50vh'}}
|
||||
scroll={sm ? {} : {y: '50vh'}}
|
||||
size={sm ? 'small' : 'large'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TldPage() {
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>('gTLD')
|
||||
const sm = useBreakpoint("sm")
|
||||
|
||||
const contentList: Record<string, React.ReactNode> = {
|
||||
sTLD: <>
|
||||
@@ -185,6 +188,7 @@ export default function TldPage() {
|
||||
activeTabKey={activeTabKey}
|
||||
key={activeTabKey}
|
||||
onTabChange={(k: string) => setActiveTabKey(k)}
|
||||
size={sm ? 'small' : 'default'}
|
||||
>
|
||||
{contentList[activeTabKey]}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import type { FormProps} from 'antd'
|
||||
import {FloatButton} from 'antd'
|
||||
import {Empty, Flex, message, Skeleton} from 'antd'
|
||||
import type {Domain} from '../../utils/api'
|
||||
import { getDomain} from '../../utils/api'
|
||||
import type {Domain, Watchlist} from '../../utils/api'
|
||||
import {addDomainToWatchlist} from '../../utils/api'
|
||||
import {getDomain} from '../../utils/api'
|
||||
import type {AxiosError} from 'axios'
|
||||
import {t} from 'ttag'
|
||||
import type { FieldType} from '../../components/search/DomainSearchBar'
|
||||
@@ -10,17 +12,19 @@ import {DomainSearchBar} from '../../components/search/DomainSearchBar'
|
||||
import {DomainResult} from '../../components/search/DomainResult'
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
import {useNavigate, useParams} from 'react-router-dom'
|
||||
import {PlusOutlined} from '@ant-design/icons'
|
||||
import WatchlistSelectionModal from '../../components/tracking/watchlist/WatchlistSelectionModal'
|
||||
|
||||
export default function DomainSearchPage() {
|
||||
const {query} = useParams()
|
||||
const [domain, setDomain] = useState<Domain | null>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const domainLdhName = domain?.ldhName
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||
navigate('/search/domain/' + values.ldhName)
|
||||
|
||||
@@ -41,7 +45,18 @@ export default function DomainSearchPage() {
|
||||
onFinish({ldhName: query, isRefreshForced: false})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
const addToWatchlist = async (watchlist: Watchlist) => {
|
||||
await addDomainToWatchlist(watchlist, domain!.ldhName).then(() => {
|
||||
setAddToWatchlistModal(false)
|
||||
|
||||
const ldhName = domain?.ldhName
|
||||
messageApi.success(t`${ldhName} added to ${watchlist.name}`)
|
||||
}).catch((e: AxiosError) => {
|
||||
showErrorAPI(e, messageApi)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
<DomainSearchBar initialValue={query} onFinish={onFinish}/>
|
||||
@@ -57,5 +72,29 @@ export default function DomainSearchPage() {
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
)
|
||||
{domain
|
||||
&& <FloatButton
|
||||
style={{
|
||||
position: 'fixed',
|
||||
insetInlineEnd: (100 - 40) / 2,
|
||||
bottom: 100 - 40 / 2
|
||||
}}
|
||||
tooltip={t`Add to Watchlist`}
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => setAddToWatchlistModal(true)}
|
||||
/>
|
||||
}
|
||||
<WatchlistSelectionModal
|
||||
open={addToWatchlistModal}
|
||||
onFinish={addToWatchlist}
|
||||
modalProps={{
|
||||
title: t`Add ${domainLdhName} to a Watchlist`,
|
||||
onCancel: () => setAddToWatchlistModal(false),
|
||||
onClose: () => setAddToWatchlistModal(false),
|
||||
cancelText: t`Cancel`,
|
||||
okText: t`Add`
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -74,8 +74,7 @@ export default function WatchlistPage() {
|
||||
return (
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
|
||||
{(connectors != null) && (watchlists != null) && watchlists.length > 0 &&
|
||||
{(connectors !== undefined) && (watchlists !== undefined) &&
|
||||
<>
|
||||
<CreateWatchlistButton onUpdateWatchlist={onCreateWatchlist} connectors={connectors} />
|
||||
<Divider/>
|
||||
|
||||
@@ -44,6 +44,13 @@ export async function patchWatchlist(token: string, watchlist: Partial<Watchlist
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function addDomainToWatchlist(watchlist: Watchlist, ldhName: string) {
|
||||
const domains = watchlist.domains.map(d => '/api/domains/' + d.ldhName)
|
||||
domains.push('/api/domains/' + ldhName)
|
||||
|
||||
return patchWatchlist(watchlist.token, {domains})
|
||||
}
|
||||
|
||||
export async function deleteWatchlist(token: string): Promise<void> {
|
||||
await request({
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -6,32 +6,38 @@ import React from 'react'
|
||||
import type {Event} from "../api"
|
||||
import {t} from "ttag"
|
||||
|
||||
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] } }) {
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Badge dot={domain.events?.find(e =>
|
||||
e.action === 'last changed' &&
|
||||
!e.deleted &&
|
||||
((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3)
|
||||
) !== undefined} color='blue' title={t`The domain name was updated less than a week ago.`}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Badge>
|
||||
</Link>
|
||||
)
|
||||
export function DomainToTag({domain, link}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] }, link?: boolean }) {
|
||||
const tag = <Badge dot={domain.events?.find(e =>
|
||||
e.action === 'last changed' &&
|
||||
!e.deleted &&
|
||||
((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3)
|
||||
) !== undefined} color='blue' title={t`The domain name was updated less than a week ago.`}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Badge>
|
||||
|
||||
if (link ?? true) {
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
{tag}
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export function statusToTag(s: string) {
|
||||
<Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
key={s}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user