feat: add eslint linter

This commit is contained in:
Maël Gangloff
2024-12-30 23:50:15 +01:00
parent ebfcc58d16
commit 99d135cc31
64 changed files with 3579 additions and 1846 deletions

View File

@@ -23,10 +23,15 @@ jobs:
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
run: >
composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
yarn install
- name: Run PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Run PHPStan
run: vendor/bin/phpstan analyse
- name: Run ESLint
run: yarn run eslint

View File

@@ -1,23 +1,23 @@
import {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, 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";
import EntitySearchPage from "./pages/search/EntitySearchPage";
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
import TldPage from "./pages/search/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 {getUser} from "./utils/api";
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
import ConnectorPage from "./pages/tracking/ConnectorPage";
import NotFoundPage from "./pages/NotFoundPage";
import useBreakpoint from "./hooks/useBreakpoint";
import {Sider} from "./components/Sider";
import {jt, t} from "ttag";
import {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, 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'
import EntitySearchPage from './pages/search/EntitySearchPage'
import NameserverSearchPage from './pages/search/NameserverSearchPage'
import TldPage from './pages/search/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 {getUser} from './utils/api'
import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
import ConnectorPage from './pages/tracking/ConnectorPage'
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 TrackedDomainPage from "./pages/tracking/TrackedDomainPage";
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
const PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
@@ -25,35 +25,33 @@ 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>
export default function App() {
export default function App(): React.ReactElement {
const navigate = useNavigate()
const location = useLocation()
const sm = useBreakpoint('sm')
const [isAuthenticated, setIsAuthenticated] = useState(false)
const authenticated = useCallback((authenticated: boolean) => {
setIsAuthenticated(authenticated)
}, []);
}, [])
const contextValue = useMemo(() => ({
authenticated,
setIsAuthenticated
}), [authenticated, setIsAuthenticated])
const [darkMode, setDarkMode] = useState(false);
const [darkMode, setDarkMode] = useState(false)
const windowQuery = window.matchMedia("(prefers-color-scheme:dark)");
const windowQuery = window.matchMedia('(prefers-color-scheme:dark)')
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
setDarkMode(event.matches)
}, [])
useEffect(() => {
windowQuery.addEventListener("change", darkModeChange)
windowQuery.addEventListener('change', darkModeChange)
return () => {
windowQuery.removeEventListener("change", darkModeChange)
windowQuery.removeEventListener('change', darkModeChange)
}
}, [windowQuery, darkModeChange])
@@ -69,8 +67,8 @@ export default function App() {
})
}, [])
return <ConfigProvider
return (
<ConfigProvider
theme={{
algorithm: darkMode ? theme.darkAlgorithm : theme.compactAlgorithm
}}
@@ -78,7 +76,7 @@ export default function App() {
<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} : {})}>
<Layout.Sider collapsible breakpoint='sm' width={220} {...(sm ? {collapsedWidth: 0} : {})}>
<Sider isAuthenticated={isAuthenticated}/>
</Layout.Sider>
<Layout>
@@ -86,32 +84,33 @@ export default function App() {
<Layout.Content style={sm ? {margin: '24px 0'} : {margin: '24px 16px 0'}}>
<div style={{
padding: 24,
minHeight: 360,
}}>
minHeight: 360
}}
>
<Routes>
<Route path="/" element={<Navigate to="/login"/>}/>
<Route path="/home" element={<TextPage resource='home.md'/>}/>
<Route path='/' element={<Navigate to='/login'/>}/>
<Route path='/home' element={<TextPage resource='home.md'/>}/>
<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='/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="/tracking/watchlist" element={<WatchlistPage/>}/>
<Route path="/tracking/domains" element={<TrackedDomainPage/>}/>
<Route path="/tracking/connectors" element={<ConnectorPage/>}/>
<Route path='/tracking/watchlist' element={<WatchlistPage/>}/>
<Route path='/tracking/domains' element={<TrackedDomainPage/>}/>
<Route path='/tracking/connectors' element={<ConnectorPage/>}/>
<Route path="/stats" element={<StatisticsPage/>}/>
<Route path="/user" element={<UserPage/>}/>
<Route path='/stats' element={<StatisticsPage/>}/>
<Route path='/user' element={<UserPage/>}/>
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
<Route path="/tos" element={<TextPage resource='tos.md'/>}/>
<Route path="/privacy" element={<TextPage resource='privacy.md'/>}/>
<Route path='/faq' element={<TextPage resource='faq.md'/>}/>
<Route path='/tos' element={<TextPage resource='tos.md'/>}/>
<Route path='/privacy' element={<TextPage resource='privacy.md'/>}/>
<Route path="/login" element={<LoginPage/>}/>
<Route path='/login' element={<LoginPage/>}/>
<Route path="*" element={<NotFoundPage/>}/>
<Route path='*' element={<NotFoundPage/>}/>
</Routes>
</div>
</Layout.Content>
@@ -120,9 +119,15 @@ export default function App() {
<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'><Button
type='text'>{t`Documentation`}</Button></Typography.Link>
<Typography.Link
target='_blank'
href='https://github.com/maelgangloff/domain-watchdog/wiki'
>
<Button
type='text'
>{t`Documentation`}
</Button>
</Typography.Link>
</Space>
<Typography.Paragraph style={{marginTop: '1em'}}>
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
@@ -134,7 +139,7 @@ export default function App() {
style={{
position: 'fixed',
insetInlineEnd: (100 - 40) / 2,
bottom: 100 - 40 / 2,
bottom: 100 - 40 / 2
}}
icon={<InfoCircleOutlined/>}
>
@@ -148,4 +153,5 @@ export default function App() {
</Layout>
</AuthenticatedContext.Provider>
</ConfigProvider>
)
}

View File

@@ -1,34 +1,29 @@
import {Button, Form, Input, message, Space} from "antd";
import {t} from "ttag";
import React, {useContext, useEffect} from "react";
import {getUser, login} from "../utils/api";
import {AuthenticatedContext} from "../pages/LoginPage";
import {useNavigate} from "react-router-dom";
import {Button, Form, Input, message, Space} from 'antd'
import {t} from 'ttag'
import React, {useContext, useEffect} from 'react'
import {getUser, login} from '../utils/api'
import {AuthenticatedContext} from '../pages/LoginPage'
import {useNavigate} from 'react-router-dom'
import {showErrorAPI} from "../utils/functions/showErrorAPI";
import {showErrorAPI} from '../utils/functions/showErrorAPI'
type FieldType = {
username: string;
password: string;
interface FieldType {
username: string
password: string
}
export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const {setIsAuthenticated} = useContext(AuthenticatedContext)
useEffect(() => {
getUser().then(() => {
setIsAuthenticated(true)
navigate('/home')
})
}, [])
const onFinish = (data: FieldType) => {
login(data.username, data.password).then(() => {
setIsAuthenticated(true)
@@ -38,19 +33,20 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
showErrorAPI(e, messageApi)
})
}
return <>
return (
<>
{contextHolder}
<Form
name="basic"
name='basic'
labelCol={{span: 8}}
wrapperCol={{span: 16}}
style={{maxWidth: 600}}
onFinish={onFinish}
autoComplete="off"
autoComplete='off'
>
<Form.Item
label={t`Email address`}
name="username"
name='username'
rules={[{required: true, message: t`Required`}]}
>
<Input autoFocus/>
@@ -58,7 +54,7 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
<Form.Item<FieldType>
label={t`Password`}
name="password"
name='password'
rules={[{required: true, message: t`Required`}]}
>
<Input.Password/>
@@ -66,16 +62,17 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
<Space>
<Form.Item wrapperCol={{offset: 8, span: 16}}>
<Button type="primary" htmlType="submit">
<Button type='primary' htmlType='submit'>
{t`Submit`}
</Button>
</Form.Item>
{ssoLogin && <Form.Item wrapperCol={{offset: 8, span: 16}}>
<Button type="dashed" htmlType="button" href="/login/oauth">
<Button type='dashed' htmlType='button' href='/login/oauth'>
{t`Log in with SSO`}
</Button>
</Form.Item>}
</Space>
</Form>
</>
)
}

View File

@@ -1,20 +1,17 @@
import {Button, Form, Input, message} from "antd";
import {t} from "ttag";
import React, {useState} from "react";
import {register} from "../utils/api";
import {useNavigate} from "react-router-dom";
import {Button, Form, Input, message} from 'antd'
import {t} from 'ttag'
import React from 'react'
import {register} from '../utils/api'
import {useNavigate} from 'react-router-dom'
import {showErrorAPI} from "../utils/functions/showErrorAPI";
import {showErrorAPI} from '../utils/functions/showErrorAPI'
type FieldType = {
username: string;
password: string;
interface FieldType {
username: string
password: string
}
export function RegisterForm() {
const [error, setError] = useState<string>()
const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
@@ -25,19 +22,20 @@ export function RegisterForm() {
showErrorAPI(e, messageApi)
})
}
return <>
return (
<>
{contextHolder}
<Form
name="basic"
name='basic'
labelCol={{span: 8}}
wrapperCol={{span: 16}}
style={{maxWidth: 600}}
onFinish={onFinish}
autoComplete="off"
autoComplete='off'
>
<Form.Item
label={t`Email address`}
name="username"
name='username'
rules={[{required: true, message: t`Required`}]}
>
<Input autoFocus/>
@@ -45,17 +43,18 @@ export function RegisterForm() {
<Form.Item<FieldType>
label={t`Password`}
name="password"
name='password'
rules={[{required: true, message: t`Required`}]}
>
<Input.Password/>
</Form.Item>
<Form.Item wrapperCol={{offset: 8, span: 16}}>
<Button block type="primary" htmlType="submit">
<Button block type='primary' htmlType='submit'>
{t`Register`}
</Button>
</Form.Item>
</Form>
</>
)
}

View File

@@ -1,5 +1,5 @@
import {ItemType} from "antd/lib/menu/interface";
import {t} from "ttag";
import {ItemType} from 'antd/lib/menu/interface'
import {t} from 'ttag'
import {
AimOutlined,
ApiOutlined,
@@ -13,10 +13,10 @@ import {
SearchOutlined,
TableOutlined,
UserOutlined
} from "@ant-design/icons";
import {Menu} from "antd";
import React from "react";
import {useLocation, useNavigate} from "react-router-dom";
} from '@ant-design/icons'
import {Menu} from 'antd'
import React from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
const navigate = useNavigate()
@@ -49,7 +49,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
title: t`TLD list`,
disabled: !isAuthenticated,
onClick: () => navigate('/search/tld')
},
}
/*
{
key: 'entity-finder',
@@ -118,7 +118,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
icon: <LogoutOutlined/>,
label: t`Log out`,
danger: true,
onClick: () => window.location.replace("/logout")
onClick: () => window.location.replace('/logout')
}])
} else {
menuItems.push({
@@ -129,12 +129,13 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
})
}
return <Menu
return (
<Menu
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
selectedKeys={[location.pathname.includes('/search/domain') ? '/search/domain' : location.pathname]}
mode="inline"
theme="dark"
mode='inline'
theme='dark'
items={menuItems}
/>
)
}

View File

@@ -1,15 +1,14 @@
import React, {useEffect} from "react";
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {Flex} from "antd";
import {Domain} from "../../utils/api";
import {getLayoutedElements} from "../tracking/watchlist/diagram/getLayoutedElements";
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from "../tracking/watchlist/diagram/watchlistToNodes";
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from "../tracking/watchlist/diagram/watchlistToEdges";
import React, {useEffect} from 'react'
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
import {Flex} from 'antd'
import {Domain} from '../../utils/api'
import {getLayoutedElements} from '../tracking/watchlist/diagram/getLayoutedElements'
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from '../tracking/watchlist/diagram/watchlistToNodes'
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from '../tracking/watchlist/diagram/watchlistToEdges'
export function DomainDiagram({domain}: { domain: Domain }) {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
useEffect(() => {
const nodes = [
@@ -33,7 +32,8 @@ export function DomainDiagram({domain}: { domain: Domain }) {
setEdges(e.edges)
}, [])
return <Flex style={{width: '100%', height: '100vh'}}>
return (
<Flex style={{width: '100%', height: '100vh'}}>
<ReactFlow
fitView
colorMode='system'
@@ -50,4 +50,5 @@ export function DomainDiagram({domain}: { domain: Domain }) {
<Background/>
</ReactFlow>
</Flex>
)
}

View File

@@ -1,21 +1,19 @@
import {StepProps, Steps, Tooltip} from "antd";
import React from "react";
import {t} from "ttag";
import {StepProps, Steps, Tooltip} from 'antd'
import React from 'react'
import {t} from 'ttag'
import {
CheckOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
FieldTimeOutlined,
SignatureOutlined
} from "@ant-design/icons";
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
} from '@ant-design/icons'
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from '../../utils/functions/rdapTranslation'
export function DomainLifecycleSteps({status}: { status: string[] }) {
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const steps: StepProps[] = [
{
title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>,
@@ -26,16 +24,19 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
icon: <CheckOutlined/>
},
{
title: <Tooltip title={rdapStatusCodeDetailTranslated["auto renew period"]}>{t`Auto-Renew Grace Period`}</Tooltip>,
title: <Tooltip
title={rdapStatusCodeDetailTranslated['auto renew period']}>{t`Auto-Renew Grace Period`}</Tooltip>,
icon: <FieldTimeOutlined style={{color: 'palevioletred'}}/>
},
{
title: <Tooltip
title={rdapStatusCodeDetailTranslated["redemption period"]}>{t`Redemption Grace Period`}</Tooltip>,
title={rdapStatusCodeDetailTranslated['redemption period']}
>{t`Redemption Grace Period`}
</Tooltip>,
icon: <ExclamationCircleOutlined style={{color: 'magenta'}}/>
},
{
title: <Tooltip title={rdapStatusCodeDetailTranslated["pending delete"]}>{t`Pending Delete`}</Tooltip>,
title: <Tooltip title={rdapStatusCodeDetailTranslated['pending delete']}>{t`Pending Delete`}</Tooltip>,
icon: <DeleteOutlined style={{color: 'orangered'}}/>
}
]
@@ -50,8 +51,10 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
currentStep = 4
}
return <Steps
return (
<Steps
current={currentStep}
items={steps}
/>
)
}

View File

@@ -1,98 +1,111 @@
import {Badge, Card, Col, Divider, Flex, Row, Space, Tag, Tooltip, Typography} from "antd";
import {t} from "ttag";
import {EventTimeline} from "./EventTimeline";
import {EntitiesList} from "./EntitiesList";
import {DomainDiagram} from "./DomainDiagram";
import React from "react";
import {Domain} from "../../utils/api";
import {rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
import {regionNames} from "../../i18n";
import {Badge, Card, Col, Divider, Flex, Row, Space, Tag, Tooltip, Typography} from 'antd'
import {t} from 'ttag'
import {EventTimeline} from './EventTimeline'
import {EntitiesList} from './EntitiesList'
import {DomainDiagram} from './DomainDiagram'
import React from 'react'
import {Domain} from '../../utils/api'
import {regionNames} from '../../i18n'
import {getCountryCode} from "../../utils/functions/getCountryCode";
import {eppStatusCodeToColor} from "../../utils/functions/eppStatusCodeToColor";
import {DomainLifecycleSteps} from "./DomainLifecycleSteps";
import {getCountryCode} from '../../utils/functions/getCountryCode'
import {DomainLifecycleSteps} from './DomainLifecycleSteps'
import {BankOutlined, KeyOutlined, SafetyCertificateOutlined} from '@ant-design/icons'
import {statusToTag} from '../tracking/StatusToTag'
export function DomainResult({domain}: { domain: Domain }) {
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const {tld, events} = domain
const domainEvents = events.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
const clientStatus = domain.status.filter(s => s.startsWith('client'))
const serverStatus = domain.status.filter(s => !clientStatus.includes(s))
const isLocked = (type: 'client' | 'server'): boolean =>
(domain.status.includes(type + ' update prohibited') && domain.status.includes(type + ' delete prohibited'))
|| domain.status.includes(type + ' transfer prohibited')
const isDomainLocked = (type: 'client' | 'server'): boolean =>
(domain.status.includes(type + ' update prohibited') && domain.status.includes(type + ' delete prohibited')) ||
domain.status.includes(type + ' transfer prohibited')
const statusToTag = (s: string) => <Tooltip
placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>
return (
<Space direction='vertical' size='middle' style={{width: '100%'}}>
return <Space direction="vertical" size="middle" style={{width: '100%'}}>
<Badge.Ribbon text={
<Badge.Ribbon
text={
<Tooltip
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}>
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}
>
{`${domain.tld.tld.toUpperCase()} (${tld.type})`}
</Tooltip>
}
color={
tld.type === 'ccTLD' ? 'purple' :
(tld.type === 'gTLD' && tld.specification13) ? "volcano" :
tld.type === 'gTLD' ? "green"
: "cyan"
}>
tld.type === 'ccTLD'
? 'purple'
: (tld.type === 'gTLD' && tld.specification13)
? 'volcano'
: tld.type === 'gTLD'
? 'green'
: 'cyan'
}
>
<Card title={<Space>
<Card
title={<Space>
{domain.ldhName}{domain.handle && <Typography.Text code>{domain.handle}</Typography.Text>}
</Space>}
size="small">
size='small'
>
{
domain.events.length > 0 && <DomainLifecycleSteps status={domain.status}/>
}
<Row gutter={8}>
<Col span={24} xl={12} xxl={12}>
<Flex justify='center' align='center' style={{margin: 10}} wrap gap="4px 0">
<Flex justify='center' align='center' style={{margin: 10}} wrap gap='4px 0'>
<Tooltip
title={t`Registry-level protection, ensuring the highest level of security by preventing unauthorized, unwanted, or accidental changes to the domain name at the registry level`}>
<Tag bordered={false} color={isLocked('server') ? 'green' : 'default'}
title={t`Registry-level protection, ensuring the highest level of security by preventing unauthorized, unwanted, or accidental changes to the domain name at the registry level`}
>
<Tag
bordered={false} color={isDomainLocked('server') ? 'green' : 'default'}
icon={<SafetyCertificateOutlined
style={{fontSize: '16px'}}/>}>{t`Registry Lock`}</Tag>
style={{fontSize: '16px'}}
/>}
>{t`Registry Lock`}
</Tag>
</Tooltip>
<Tooltip
title={t`Registrar-level protection, safeguarding the domain from unauthorized, unwanted, or accidental changes through registrar controls`}>
<Tag bordered={false} color={isLocked('client') ? 'green' : 'default'}
title={t`Registrar-level protection, safeguarding the domain from unauthorized, unwanted, or accidental changes through registrar controls`}
>
<Tag
bordered={false} color={isDomainLocked('client') ? 'green' : 'default'}
icon={<BankOutlined
style={{fontSize: '16px'}}/>}>{t`Registrar Lock`}</Tag>
style={{fontSize: '16px'}}
/>}
>{t`Registrar Lock`}
</Tag>
</Tooltip>
<Tooltip
title={t`DNSSEC secures DNS by adding cryptographic signatures to DNS records, ensuring authenticity and integrity of responses`}>
<Tag bordered={false} color={domain.delegationSigned ? 'green' : 'default'}
icon={<KeyOutlined style={{fontSize: '16px'}}/>}>{t`DNSSEC`}</Tag>
title={t`DNSSEC secures DNS by adding cryptographic signatures to DNS records, ensuring authenticity and integrity of responses`}
>
<Tag
bordered={false} color={domain.delegationSigned ? 'green' : 'default'}
icon={<KeyOutlined style={{fontSize: '16px'}}/>}
>{t`DNSSEC`}
</Tag>
</Tooltip>
</Flex>
{domain.status.length > 0 &&
<>
<Divider orientation="left">{t`EPP Status Codes`}</Divider>
<Flex gap="4px 0" wrap>
<Divider orientation='left'>{t`EPP Status Codes`}</Divider>
<Flex gap='4px 0' wrap>
{serverStatus.map(statusToTag)}
{clientStatus.map(statusToTag)}
</Flex>
</>
}
</>}
{
domain.events.length > 0 && <>
<Divider orientation="left">{t`Timeline`}</Divider>
<Divider orientation='left'>{t`Timeline`}</Divider>
<EventTimeline events={domainEvents}/>
</>
}
{
domain.entities.length > 0 &&
<>
<Divider orientation="left">{t`Entities`}</Divider>
<Divider orientation='left'>{t`Entities`}</Divider>
<EntitiesList domain={domain}/>
</>
}
@@ -104,4 +117,5 @@ export function DomainResult({domain}: { domain: Domain }) {
</Card>
</Badge.Ribbon>
</Space>
)
}

View File

@@ -1,20 +1,24 @@
import {Form, Input} from "antd";
import {t} from "ttag";
import {SearchOutlined} from "@ant-design/icons";
import React from "react";
import {Form, Input} from 'antd'
import {t} from 'ttag'
import {SearchOutlined} from '@ant-design/icons'
import React from 'react'
export type FieldType = {
export interface FieldType {
ldhName: string
}
export function DomainSearchBar({onFinish, initialValue}: { onFinish: (values: FieldType) => void, initialValue?: string }) {
return <Form
export function DomainSearchBar({onFinish, initialValue}: {
onFinish: (values: FieldType) => void,
initialValue?: string
}) {
return (
<Form
onFinish={onFinish}
autoComplete="off"
autoComplete='off'
style={{width: '100%'}}
>
<Form.Item<FieldType>
name="ldhName"
name='ldhName'
initialValue={initialValue}
rules={[{
required: true,
@@ -26,13 +30,15 @@ export function DomainSearchBar({onFinish, initialValue}: { onFinish: (values: F
min: 2
}]}
>
<Input style={{textAlign: 'center'}}
size="large"
<Input
style={{textAlign: 'center'}}
size='large'
prefix={<SearchOutlined/>}
placeholder="example.com"
placeholder='example.com'
autoComplete='off'
autoFocus
/>
</Form.Item>
</Form>
)
}

View File

@@ -1,30 +1,33 @@
import {List, Tag, Tooltip, Typography} from "antd";
import React from "react";
import {Domain} from "../../utils/api";
import {rdapRoleDetailTranslation, rdapRoleTranslation} from "../../utils/functions/rdapTranslation";
import {roleToAvatar} from "../../utils/functions/roleToAvatar";
import {rolesToColor} from "../../utils/functions/rolesToColor";
import {sortDomainEntities} from "../../utils/functions/sortDomainEntities";
import {extractDetailsFromJCard} from "../../utils/functions/extractDetailsFromJCard";
import {List, Tag, Tooltip, Typography} from 'antd'
import React from 'react'
import {Domain} from '../../utils/api'
import {rdapRoleDetailTranslation, rdapRoleTranslation} from '../../utils/functions/rdapTranslation'
import {roleToAvatar} from '../../utils/functions/roleToAvatar'
import {rolesToColor} from '../../utils/functions/rolesToColor'
import {sortDomainEntities} from '../../utils/functions/sortDomainEntities'
import {extractDetailsFromJCard} from '../../utils/functions/extractDetailsFromJCard'
export function EntitiesList({domain}: { domain: Domain }) {
const rdapRoleTranslated = rdapRoleTranslation()
const rdapRoleDetailTranslated = rdapRoleDetailTranslation()
const roleToTag = (r: string) => <Tooltip
title={rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] || undefined}>
<Tag color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r
}</Tag>
title={rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] || undefined}
>
<Tag color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r}
</Tag>
</Tooltip>
return <List
className="demo-loadmore-list"
itemLayout="horizontal"
return (
<List
className='demo-loadmore-list'
itemLayout='horizontal'
dataSource={sortDomainEntities(domain)}
renderItem={(e) => {
const details = extractDetailsFromJCard(e)
return <List.Item>
return (
<List.Item>
<List.Item.Meta
avatar={roleToAvatar(e)}
title={<Typography.Text code>{e.entity.handle}</Typography.Text>}
@@ -35,7 +38,8 @@ export function EntitiesList({domain}: { domain: Domain }) {
/>
{e.roles.map(roleToTag)}
</List.Item>
}
}
)
}}
/>
)
}

View File

@@ -1,10 +1,10 @@
import {Timeline, Tooltip, Typography} from "antd";
import React from "react";
import {Event} from "../../utils/api";
import useBreakpoint from "../../hooks/useBreakpoint";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../utils/functions/rdapTranslation";
import {actionToColor} from "../../utils/functions/actionToColor";
import {actionToIcon} from "../../utils/functions/actionToIcon";
import {Timeline, Tooltip, Typography} from 'antd'
import React from 'react'
import {Event} from '../../utils/api'
import useBreakpoint from '../../hooks/useBreakpoint'
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../utils/functions/rdapTranslation'
import {actionToColor} from '../../utils/functions/actionToColor'
import {actionToIcon} from '../../utils/functions/actionToIcon'
export function EventTimeline({events}: { events: Event[] }) {
const sm = useBreakpoint('sm')
@@ -13,27 +13,35 @@ export function EventTimeline({events}: { events: Event[] }) {
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
return <>
return (
<>
<Timeline
mode={sm ? "left" : "right"}
mode={sm ? 'left' : 'right'}
items={events.map(e => {
const eventName = <Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
const eventName = (
<Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
{rdapEventNameTranslated[e.action as keyof typeof rdapEventNameTranslated] || e.action}
</Typography.Text>
)
const dateStr = <Typography.Text
style={{color: e.deleted ? 'grey' : 'default'}}>{new Date(e.date).toLocaleString(locale)}
const dateStr = (
<Typography.Text
style={{color: e.deleted ? 'grey' : 'default'}}
>{new Date(e.date).toLocaleString(locale)}
</Typography.Text>
)
const eventDetail = rdapEventDetailTranslated[e.action as keyof typeof rdapEventDetailTranslated] || undefined
const text = sm ? {
const text = sm
? {
children: <Tooltip placement='bottom' title={eventDetail}>
{eventName}&emsp;{dateStr}
</Tooltip>
} : {
}
: {
label: dateStr,
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>,
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>
}
return {
@@ -43,8 +51,8 @@ export function EventTimeline({events}: { events: Event[] }) {
...text
}
}
)
}
)}
/>
</>
)
}

View File

@@ -1,21 +1,29 @@
import {Tag} from "antd";
import {DeleteOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import punycode from "punycode/punycode";
import {Link} from "react-router-dom";
import React from "react";
import {Tag} from 'antd'
import {DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
import punycode from 'punycode/punycode'
import {Link} from 'react-router-dom'
import React from 'react'
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[] } }) {
return <Link to={'/search/domain/' + domain.ldhName}>
return (
<Link to={'/search/domain/' + domain.ldhName}>
<Tag
color={
domain.deleted ? 'magenta' :
domain.status.includes('redemption period') ? 'yellow' :
domain.status.includes('pending delete') ? 'volcano' : 'default'
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>
</Link>
domain.deleted
? <DeleteOutlined/>
: domain.status.includes('redemption period')
? <ExclamationCircleOutlined/>
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
}
>{punycode.toUnicode(domain.ldhName)}
</Tag>
</Link>
)
}

View File

@@ -0,0 +1,17 @@
import {Tag, Tooltip} from 'antd'
import {eppStatusCodeToColor} from '../../utils/functions/eppStatusCodeToColor'
import React from 'react'
import {rdapStatusCodeDetailTranslation} from '../../utils/functions/rdapTranslation'
export function statusToTag(s: string) {
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
return (
<Tooltip
placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>
)
}

View File

@@ -1,21 +1,21 @@
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
import React, {useState} from "react";
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
import {t} from "ttag";
import {BankOutlined} from "@ant-design/icons";
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from 'antd'
import React, {useState} from 'react'
import {Connector, ConnectorProvider} from '../../../utils/api/connectors'
import {t} from 'ttag'
import {BankOutlined} from '@ant-design/icons'
import {
ovhEndpointList as ovhEndpointListFunction,
ovhFields as ovhFieldsFunction,
ovhPricingMode as ovhPricingModeFunction,
ovhSubsidiaryList as ovhSubsidiaryListFunction
} from "../../../utils/providers/ovh";
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
} from '../../../utils/providers/ovh'
import {helpGetTokenLink, tosHyperlink} from '../../../utils/providers'
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
sm: {span: 20, offset: 4}
}
}
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
@@ -27,18 +27,18 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
const [open, setOpen] = useState(false)
const [ovhPricingModeValue, setOvhPricingModeValue] = useState<string | undefined>()
return <Form
return (
<Form
{...formItemLayoutWithOutLabel}
form={form}
layout="horizontal"
layout='horizontal'
labelCol={{span: 6}}
wrapperCol={{span: 14}}
onFinish={onCreate}
>
<Form.Item
label={t`Provider`}
name="provider"
name='provider'
help={helpGetTokenLink(provider)}
rules={[{required: true, message: t`Required`}]}
>
@@ -50,9 +50,9 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
value: ConnectorProvider[c as keyof typeof ConnectorProvider],
label: (
<>
<BankOutlined/>{" "} {c}
<BankOutlined/>{' '} {c}
</>
),
)
}))}
value={provider}
onChange={setProvider}
@@ -64,6 +64,7 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
provider === ConnectorProvider.OVH && <>
{
Object.keys(ovhFields).map(fieldName => <Form.Item
key={ovhFields[fieldName as keyof typeof ovhFields]}
label={ovhFields[fieldName as keyof typeof ovhFields]}
name={['authData', fieldName]}
rules={[{required: true, message: t`Required`}]}
@@ -76,14 +77,14 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
name={['authData', 'apiEndpoint']}
rules={[{required: true, message: t`Required`}]}
>
<Select options={ovhEndpointList} optionFilterProp="label"/>
<Select options={ovhEndpointList} optionFilterProp='label'/>
</Form.Item>
<Form.Item
label={t`OVH subsidiary`}
name={['authData', 'ovhSubsidiary']}
rules={[{required: true, message: t`Required`}]}
>
<Select options={ovhSubsidiaryList} optionFilterProp="label"/>
<Select options={ovhSubsidiaryList} optionFilterProp='label'/>
</Form.Item>
<Form.Item
@@ -102,14 +103,16 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
onConfirm={() => setOpen(false)}
open={open}
>
<Select options={ovhPricingMode} optionFilterProp="label" value={ovhPricingModeValue}
<Select
options={ovhPricingMode} optionFilterProp='label' value={ovhPricingModeValue}
onChange={(value: string) => {
setOvhPricingModeValue(value)
form.setFieldValue(['authData', 'pricingMode'], value)
if (value !== 'create-default') {
setOpen(true)
}
}}/>
}}
/>
</Popconfirm>
</Form.Item>
</>
@@ -119,15 +122,19 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
<Form.Item
label={t`Personal Access Token (PAT)`}
name={['authData', 'token']}
rules={[{required: true, message: t`Required`}]}>
rules={[{required: true, message: t`Required`}]}
>
<Input autoComplete='off'/>
</Form.Item>
<Form.Item
label={t`Organization sharing ID`}
name={['authData', 'sharingId']}
help={<Typography.Text
type='secondary'>{t`It indicates the organization that will pay for the ordered product`}</Typography.Text>}
required={false}>
type='secondary'
>{t`It indicates the organization that will pay for the ordered product`}
</Typography.Text>}
required={false}
>
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
</Form.Item>
</>
@@ -136,41 +143,54 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
provider === ConnectorProvider.AUTODNS && <>
<Alert
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
type="warning"/>
type='warning'
/>
<br/>
<Form.Item
label={t`AutoDNS Username`}
name={['authData', 'username']}
help={<Typography.Text
type='secondary'>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}</Typography.Text>}
rules={[{required: true, message: t`Required`}]}>
<Input autoComplete='off' required={true}/>
type='secondary'
>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}
</Typography.Text>}
rules={[{required: true, message: t`Required`}]}
>
<Input autoComplete='off' required/>
</Form.Item>
<Form.Item
label={t`AutoDNS Password`}
name={['authData', 'password']}
rules={[{required: true, message: t`Required`}]}
required={true}>
<Input.Password autoComplete='off' required={true} placeholder=''/>
required
>
<Input.Password autoComplete='off' required placeholder=''/>
</Form.Item>
<Form.Item
label={t`Owner nic-handle`}
name={['authData', 'contactid']}
help={<Typography.Text
type='secondary'>{t`The nic-handle of the domain name owner`}<a
href="https://cloud.autodns.com/contacts/domain">{t`You can get it from this page`}</a></Typography.Text>}
type='secondary'
>{t`The nic-handle of the domain name owner`}<a
href='https://cloud.autodns.com/contacts/domain'
>{t`You can get it from this page`}
</a>
</Typography.Text>}
rules={[{required: true, message: t`Required`}]}
required={true}>
<Input autoComplete='off' required={true} placeholder=''/>
required
>
<Input autoComplete='off' required placeholder=''/>
</Form.Item>
<Form.Item
label={t`Context Value`}
name={['authData', 'context']}
help={<Typography.Text
type='secondary'>{t`If you not sure, use the default value 4`}</Typography.Text>}
type='secondary'
>{t`If you not sure, use the default value 4`}
</Typography.Text>}
required={false}>
required={false}
>
<Input autoComplete='off' required={false} placeholder='4'/>
</Form.Item>
@@ -182,7 +202,9 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
rules={[{required: true, message: t`Required`}]}
>
<Checkbox
required={true}>{t`Owner confirms his consent of domain order jobs`}</Checkbox>
required
>{t`Owner confirms his consent of domain order jobs`}
</Checkbox>
</Form.Item>
</>
@@ -193,13 +215,13 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
label={t`Username`}
name={['authData', 'ApiUser']}
>
<Input autoComplete='off'></Input>
<Input autoComplete='off'/>
</Form.Item>
<Form.Item
label={t`API key`}
name={['authData', 'ApiKey']}
>
<Input autoComplete='off'></Input>
<Input autoComplete='off'/>
</Form.Item>
</>
}
@@ -207,49 +229,55 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
{
provider !== undefined && <>
<Form.Item
valuePropName="checked"
valuePropName='checked'
label={t`API Terms of Service`}
name={['authData', 'acceptConditions']}
rules={[{required: true, message: t`Required`}]}
style={{marginTop: '3em'}}
>
<Checkbox
required={true}>
required
>
<Typography.Link target='_blank' href={tosHyperlink(provider)}>
{t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
</Typography.Link>
</Checkbox>
</Form.Item>
<Form.Item
valuePropName="checked"
valuePropName='checked'
label={t`Legal age`}
name={['authData', 'ownerLegalAge']}
rules={[{required: true, message: t`Required`}]}
>
<Checkbox
required={true}>{t`I am of the minimum age required to consent to these conditions`}</Checkbox>
required
>{t`I am of the minimum age required to consent to these conditions`}
</Checkbox>
</Form.Item>
<Form.Item
valuePropName="checked"
valuePropName='checked'
label={t`Withdrawal period`}
name={['authData', 'waiveRetractationPeriod']}
rules={[{required: true, message: t`Required`}]}
>
<Checkbox
required={true}>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}</Checkbox>
required
>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}
</Checkbox>
</Form.Item>
</>
}
<Form.Item style={{marginTop: '5vh'}}>
<Space>
<Button type="primary" htmlType="submit">
<Button type='primary' htmlType='submit'>
{t`Create`}
</Button>
<Button type="default" htmlType="reset">
<Button type='default' htmlType='reset'>
{t`Reset`}
</Button>
</Space>
</Form.Item>
</Form>
)
}

View File

@@ -1,11 +1,10 @@
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Connector, deleteConnector} from "../../../utils/api/connectors";
const {useToken} = theme;
import {Card, Divider, message, Popconfirm, theme, Typography} from 'antd'
import {t} from 'ttag'
import {DeleteFilled} from '@ant-design/icons'
import React from 'react'
import {Connector, deleteConnector} from '../../../utils/api/connectors'
const {useToken} = theme
export type ConnectorElement = Connector & { id: string, createdAt: string }
@@ -13,28 +12,36 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
const {token} = useToken()
const [messageApi, contextHolder] = message.useMessage()
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
const onConnectorDelete = async (connector: ConnectorElement) => await deleteConnector(connector.id)
.then(onDelete)
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
return <>
return (
<>
{connectors.map(connector =>
<>
{contextHolder}
<Card hoverable title={<Typography.Text
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>}
<Card
hoverable title={<Typography.Text
title={new Date(connector.createdAt).toLocaleString()}
>{t`Connector ${connector.provider}`}
</Typography.Text>}
size='small'
style={{width: '100%'}}
extra={<Popconfirm title={t`Delete the Connector`}
extra={<Popconfirm
title={t`Delete the Connector`}
description={t`Are you sure to delete this Connector?`}
onConfirm={() => onConnectorDelete(connector)}
onConfirm={async () => await onConnectorDelete(connector)}
okText={t`Yes`}
cancelText={t`No`}
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>
><DeleteFilled style={{color: token.colorError}}/>
</Popconfirm>}
>
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/>
</Card>
<Divider/>
</>
)}
</>
)
}

View File

@@ -1,22 +1,26 @@
import {CalendarFilled} from "@ant-design/icons";
import {t} from "ttag";
import {Popover, QRCode, Typography} from "antd";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {CalendarFilled} from '@ant-design/icons'
import {t} from 'ttag'
import {Popover, QRCode, Typography} from 'antd'
import React from 'react'
import {Watchlist} from '../../../utils/api'
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
return <Typography.Link href={icsResourceLink}>
<Popover content={<QRCode value={icsResourceLink}
return (
<Typography.Link href={icsResourceLink}>
<Popover content={<QRCode
value={icsResourceLink}
bordered={false}
title={t`QR Code for iCalendar export`}
type='svg'
/>}>
<CalendarFilled title={t`Export events to iCalendar format`}
/>}
>
<CalendarFilled
title={t`Export events to iCalendar format`}
style={{color: 'limegreen'}}
/>
</Popover>
</Typography.Link>
)
}

View File

@@ -1,22 +1,24 @@
import {Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {deleteWatchlist} from "../../../utils/api";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Popconfirm, theme, Typography} from 'antd'
import {t} from 'ttag'
import {deleteWatchlist, Watchlist} from '../../../utils/api'
import {DeleteFilled} from '@ant-design/icons'
import React from 'react'
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
const {token} = theme.useToken()
return <Popconfirm
return (
<Popconfirm
title={t`Delete the Watchlist`}
description={t`Are you sure to delete this Watchlist?`}
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
onConfirm={async () => await deleteWatchlist(watchlist.token).then(onDelete)}
okText={t`Yes`}
cancelText={t`No`}
okButtonProps={{danger: true}}>
okButtonProps={{danger: true}}
>
<Typography.Link>
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
</Typography.Link>
</Popconfirm>
)
}

View File

@@ -1,27 +1,41 @@
import React, {ReactElement, useEffect, useState} from "react";
import {Domain, getTrackedDomainList} from "../../../utils/api";
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from "antd";
import {t} from "ttag";
import {ColumnType} from "antd/es/table";
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
import {Link} from "react-router-dom";
import React, {ReactElement, useEffect, useState} from 'react'
import {Domain, getTrackedDomainList} from '../../../utils/api'
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from 'antd'
import {t} from 'ttag'
import {ColumnType} from 'antd/es/table'
import {rdapStatusCodeDetailTranslation} from '../../../utils/functions/rdapTranslation'
import {eppStatusCodeToColor} from '../../../utils/functions/eppStatusCodeToColor'
import {Link} from 'react-router-dom'
import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons'
import {DomainToTag} from "../DomainToTag";
import {DomainToTag} from '../DomainToTag'
export function TrackedDomainTable() {
const REDEMPTION_NOTICE = <Tooltip
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}>
const REDEMPTION_NOTICE = (
<Tooltip
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}
>
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
</Tooltip>
)
const PENDING_DELETE_NOTICE = <Tooltip
title={t`At least one domain name is pending deletion and will soon become available for registration again`}>
const PENDING_DELETE_NOTICE = (
<Tooltip
title={t`At least one domain name is pending deletion and will soon become available for registration again`}
>
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
</Tooltip>
)
const [dataTable, setDataTable] = useState<(Domain & { domain: Domain })[]>([])
interface TableRow {
key: string
ldhName: ReactElement
expirationDate: string
status: ReactElement[]
updatedAt: string
domain: Domain
}
const [dataTable, setDataTable] = useState<TableRow[]>([])
const [total, setTotal] = useState<number>()
const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([])
@@ -46,8 +60,10 @@ export function TrackedDomainTable() {
ldhName: <DomainToTag domain={d}/>,
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
status: d.status.map(s => <Tooltip
key={s}
placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>
),
@@ -63,17 +79,19 @@ export function TrackedDomainTable() {
fetchData({page: 1, itemsPerPage: 30})
}, [])
interface RecordType {
domain: Domain
}
const columns: ColumnType<any>[] = [
const columns: Array<ColumnType<RecordType>> = [
{
title: t`Domain`,
dataIndex: "ldhName"
dataIndex: 'ldhName'
},
{
title: t`Expiration date`,
dataIndex: 'expirationDate',
sorter: (a: { domain: Domain }, b: { domain: Domain }) => {
sorter: (a: RecordType, b: RecordType) => {
const expirationDate1 = a.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
const expirationDate2 = b.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
@@ -85,47 +103,51 @@ export function TrackedDomainTable() {
{
title: t`Updated at`,
dataIndex: 'updatedAt',
sorter: (a: { domain: Domain }, b: {
domain: Domain
}) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
sorter: (a: RecordType, b: RecordType) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
},
{
title: t`Status`,
dataIndex: 'status',
showSorterTooltip: {target: 'full-header'},
filters: [...new Set(dataTable.map((d: any) => d.domain.status).flat())].map(s => ({
filters: [...new Set(dataTable.map((d: RecordType) => d.domain.status).flat())].map(s => ({
text: <Tooltip
placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>,
value: s,
value: s
})),
onFilter: (value, record: { domain: Domain }) => record.domain.status.includes(value as string)
onFilter: (value, record: RecordType) => record.domain.status.includes(value as string)
}
]
return <>
return (
<>
{
total === 0 ? <Empty
total === 0
? <Empty
description={t`No tracked domain names were found, please create your first Watchlist`}
>
<Link to='/tracking/watchlist'>
<Button type="primary">Create Now</Button>
<Button type='primary'>Create Now</Button>
</Link>
</Empty> : <Skeleton loading={total === undefined}>
</Empty>
: <Skeleton loading={total === undefined}>
<Result
style={{paddingTop: 0}}
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
{...(specialNotice.length > 0 ? {
{...(specialNotice.length > 0
? {
icon: <ExceptionOutlined/>,
status: 'warning',
title: t`At least one domain name you are tracking requires special attention`,
extra: specialNotice
} : {
}
: {
icon: <MonitorOutlined/>,
status: 'info',
title: t`The domain names below are subject to special monitoring`,
title: t`The domain names below are subject to special monitoring`
})}
/>
@@ -146,4 +168,5 @@ export function TrackedDomainTable() {
</Skeleton>
}
</>
)
}

View File

@@ -1,22 +1,20 @@
import {Button, Drawer, Form, Typography} from "antd";
import {t} from "ttag";
import {WatchlistForm} from "./WatchlistForm";
import React, {useState} from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {EditOutlined} from "@ant-design/icons";
import {Connector} from "../../../utils/api/connectors";
import {Button, Drawer, Form, Typography} from 'antd'
import {t} from 'ttag'
import {WatchlistForm} from './WatchlistForm'
import React, {useState} from 'react'
import {EditOutlined} from '@ant-design/icons'
import {Connector} from '../../../utils/api/connectors'
import {Watchlist} from '../../../utils/api'
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
watchlist: Watchlist
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
connectors: Array<Connector & { id: string }>
}) {
const [form] = Form.useForm()
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const showDrawer = () => {
setOpen(true)
}
@@ -26,9 +24,11 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
setLoading(false)
}
return <>
return (
<>
<Typography.Link>
<EditOutlined title={t`Edit the Watchlist`} onClick={() => {
<EditOutlined
title={t`Edit the Watchlist`} onClick={() => {
showDrawer()
form.setFields([
{name: 'token', value: watchlist.token},
@@ -38,7 +38,8 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
{name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
{name: 'dsn', value: watchlist.dsn}
])
}}/>
}}
/>
</Typography.Link>
<Drawer
title={t`Update a Watchlist`}
@@ -48,7 +49,7 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
loading={loading}
styles={{
body: {
paddingBottom: 80,
paddingBottom: 80
}
}}
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
@@ -64,5 +65,5 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
/>
</Drawer>
</>
)
}

View File

@@ -1,46 +1,45 @@
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from "antd";
import {DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
import {t} from "ttag";
import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Connector} from "../../../utils/api/connectors";
import useBreakpoint from "../../../hooks/useBreakpoint";
import {CalendarWatchlistButton} from "./CalendarWatchlistButton";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from 'antd'
import {DisconnectOutlined, LinkOutlined} from '@ant-design/icons'
import {t} from 'ttag'
import {ViewDiagramWatchlistButton} from './diagram/ViewDiagramWatchlistButton'
import {UpdateWatchlistButton} from './UpdateWatchlistButton'
import {DeleteWatchlistButton} from './DeleteWatchlistButton'
import React from 'react'
import {Connector} from '../../../utils/api/connectors'
import {CalendarWatchlistButton} from './CalendarWatchlistButton'
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
import {actionToColor} from "../../../utils/functions/actionToColor";
import {DomainToTag} from "../DomainToTag";
import {actionToColor} from '../../../utils/functions/actionToColor'
import {DomainToTag} from '../DomainToTag'
import {Watchlist} from '../../../utils/api'
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[],
watchlist: Watchlist
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
connectors: Array<Connector & { id: string }>
onDelete: () => void
}) {
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
return <>
return (
<>
<Card
type='inner'
title={<>
{
watchlist.connector ?
<Tooltip title={watchlist.connector.id}>
<Tag icon={<LinkOutlined/>} color="lime-inverse"/>
</Tooltip> :
<Tooltip title={t`This Watchlist is not linked to a Connector.`}>
<Tag icon={<DisconnectOutlined/>} color="default"/>
(watchlist.connector != null)
? <Tooltip title={watchlist.connector.id}>
<Tag icon={<LinkOutlined/>} color='lime-inverse'/>
</Tooltip>
: <Tooltip title={t`This Watchlist is not linked to a Connector.`}>
<Tag icon={<DisconnectOutlined/>} color='default'/>
</Tooltip>
}
<Tooltip title={new Date(watchlist.createdAt).toLocaleString()}>
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
</Tooltip>
</>
}
</>}
size='small'
style={{width: '100%'}}
extra={
@@ -62,12 +61,14 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
<Row gutter={16}>
<Col span={16}>
{watchlist.domains.map(d => <DomainToTag domain={d}/>)}
{watchlist.domains.map(d => <DomainToTag key={d.ldhName} domain={d}/>)}
</Col>
<Col span={8}>
{watchlist.triggers?.filter(t => t.action === 'email')
.map(t => <Tooltip
title={rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] || undefined}>
key={t.event}
title={rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] || undefined}
>
<Tag color={actionToColor(t.event)}>
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
</Tag>
@@ -78,4 +79,5 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet
</Card>
<Divider/>
</>
)
}

View File

@@ -1,49 +1,55 @@
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
import {t} from "ttag";
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
import React from "react";
import {Connector} from "../../../utils/api/connectors";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
import {actionToColor} from "../../../utils/functions/actionToColor";
import {actionToIcon} from "../../../utils/functions/actionToIcon";
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from 'antd'
import {t} from 'ttag'
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons'
import React from 'react'
import {Connector} from '../../../utils/api/connectors'
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
import {actionToColor} from '../../../utils/functions/actionToColor'
import {actionToIcon} from '../../../utils/functions/actionToIcon'
import {EventAction} from '../../../utils/api'
type TagRender = SelectProps['tagRender'];
type TagRender = SelectProps['tagRender']
const formItemLayout = {
labelCol: {
xs: {span: 24},
sm: {span: 4},
sm: {span: 4}
},
wrapperCol: {
xs: {span: 24},
sm: {span: 20},
},
};
sm: {span: 20}
}
}
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
};
sm: {span: 20, offset: 4}
}
}
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
form: FormInstance,
connectors: (Connector & { id: string })[]
form: FormInstance
connectors: Array<Connector & { id: string }>
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
isCreation: boolean
}) {
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const triggerTagRenderer: TagRender = (props) => {
const {value, closable, onClose} = props;
const triggerTagRenderer: TagRender = ({value, closable, onClose}: {
value: EventAction
closable: boolean
onClose: () => void
}) => {
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault()
event.stopPropagation()
}
return (<Tooltip
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}>
return (
<Tooltip
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}
>
<Tag
icon={actionToIcon(value)}
color={actionToColor(value)}
@@ -58,7 +64,8 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
)
}
return <Form
return (
<Form
{...formItemLayoutWithOutLabel}
form={form}
onFinish={onFinish}
@@ -69,33 +76,35 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
<Input hidden/>
</Form.Item>
<Form.Item label={t`Name`}
<Form.Item
label={t`Name`}
name='name'
labelCol={{
xs: {span: 24},
sm: {span: 4},
sm: {span: 4}
}}
wrapperCol={{
md: {span: 12},
sm: {span: 20},
sm: {span: 20}
}}
>
<Input placeholder={t`Watchlist Name`}
<Input
placeholder={t`Watchlist Name`}
title={t`Naming the Watchlist makes it easier to find in the list below.`}
autoComplete='off'
autoFocus
/>
</Form.Item>
<Form.List
name="domains"
name='domains'
rules={[
{
validator: async (_, domains) => {
if (!domains || domains.length < 1) {
return Promise.reject(new Error(t`At least one domain name`));
return await Promise.reject(new Error(t`At least one domain name`))
}
}
}
},
},
]}
>
{(fields, {add, remove}, {errors}) => (
@@ -104,7 +113,7 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? t`Domain names` : ''}
required={true}
required
key={field.key}
>
<Form.Item
@@ -123,17 +132,19 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
>
<Input placeholder={t`Domain name`} style={{width: '60%'}} autoComplete='off'/>
</Form.Item>
{fields.length > 1 ? (
{fields.length > 1
? (
<MinusCircleOutlined
className="dynamic-delete-button"
className='dynamic-delete-button'
onClick={() => remove(field.name)}
/>
) : null}
)
: null}
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
type='dashed'
onClick={() => add()}
style={{width: '60%'}}
icon={<PlusOutlined/>}
@@ -145,21 +156,22 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
</>
)}
</Form.List>
<Form.Item label={t`Tracked events`}
<Form.Item
label={t`Tracked events`}
name='triggers'
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
labelCol={{
xs: {span: 24},
sm: {span: 4},
sm: {span: 4}
}}
wrapperCol={{
md: {span: 12},
sm: {span: 20},
sm: {span: 20}
}}
required
>
<Select
mode="multiple"
mode='multiple'
tagRender={triggerTagRenderer}
style={{width: '100%'}}
options={Object.keys(rdapEventNameTranslated).map(e => ({
@@ -170,23 +182,25 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
/>
</Form.Item>
<Form.Item label={t`Connector`}
<Form.Item
label={t`Connector`}
name='connector'
labelCol={{
xs: {span: 24},
sm: {span: 4},
sm: {span: 4}
}}
wrapperCol={{
md: {span: 12},
sm: {span: 20},
sm: {span: 20}
}}
help={t`Please make sure the connector information is valid to purchase a domain that may be available soon.`}
>
<Select showSearch
<Select
showSearch
allowClear
placeholder={t`Connector`}
suffixIcon={<ApiOutlined/>}
optionFilterProp="label"
optionFilterProp='label'
options={connectors.map(c => ({
label: `${c.provider} (${c.id})`,
value: c.id
@@ -194,14 +208,15 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
/>
</Form.Item>
<Form.List
name="dsn">
name='dsn'
>
{(fields, {add, remove}, {errors}) => (
<>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? t`DSN` : ''}
required={true}
required
key={field.key}
>
<Form.Item
@@ -216,25 +231,32 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
}]}
noStyle
>
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
autoComplete='off'/>
<Input
placeholder='slack://TOKEN@default?channel=CHANNEL' style={{width: '60%'}}
autoComplete='off'
/>
</Form.Item>
{fields.length > 0 ? (
{fields.length > 0
? (
<MinusCircleOutlined
className="dynamic-delete-button"
className='dynamic-delete-button'
onClick={() => remove(field.name)}
/>
) : null}
)
: null}
</Form.Item>
))}
<Form.Item help={
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'
target='_blank'>
<Typography.Link
href='https://symfony.com/doc/current/notifier.html#chat-channel'
target='_blank'
>
{t`Check out this link to the Symfony documentation to help you build the DSN`}
</Typography.Link>}
</Typography.Link>
}
>
<Button
type="dashed"
type='dashed'
onClick={() => add()}
style={{width: '60%'}}
icon={<PlusOutlined/>}
@@ -248,13 +270,14 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
</Form.List>
<Form.Item style={{marginTop: '5vh'}}>
<Space>
<Button type="primary" htmlType="submit">
<Button type='primary' htmlType='submit'>
{isCreation ? t`Create` : t`Update`}
</Button>
<Button type="default" htmlType="reset">
<Button type='default' htmlType='reset'>
{t`Reset`}
</Button>
</Space>
</Form.Item>
</Form>
)
}

View File

@@ -1,23 +1,25 @@
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Connector} from "../../../utils/api/connectors";
import {WatchlistCard} from "./WatchlistCard";
import React from 'react'
import {Connector} from '../../../utils/api/connectors'
import {WatchlistCard} from './WatchlistCard'
import {Watchlist} from '../../../utils/api'
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
watchlists: Watchlist[],
onDelete: () => void,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
watchlists: Watchlist[]
onDelete: () => void
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
connectors: Array<Connector & { id: string }>
}) {
return <>
return (
<>
{watchlists.map(watchlist =>
<WatchlistCard watchlist={watchlist}
<WatchlistCard
key={watchlist.token}
watchlist={watchlist}
onUpdateWatchlist={onUpdateWatchlist}
connectors={connectors}
onDelete={onDelete}/>
onDelete={onDelete}
/>
)}
</>
)
}
</>
}

View File

@@ -1,21 +1,20 @@
import {Button, Flex, Modal, Space, Typography} from "antd"
import {t} from "ttag"
import React, {useEffect, useState} from "react"
import {ApartmentOutlined} from "@ant-design/icons"
import {Button, Flex, Modal, Space, Typography} from 'antd'
import {t} from 'ttag'
import React, {useEffect, useState} from 'react'
import {ApartmentOutlined} from '@ant-design/icons'
import '@xyflow/react/dist/style.css'
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {getWatchlist} from "../../../../utils/api";
import {getLayoutedElements} from "./getLayoutedElements";
import {watchlistToNodes} from "./watchlistToNodes";
import {watchlistToEdges} from "./watchlistToEdges";
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
import {getWatchlist} from '../../../../utils/api'
import {getLayoutedElements} from './getLayoutedElements'
import {watchlistToNodes} from './watchlistToNodes'
import {watchlistToEdges} from './watchlistToEdges'
export function ViewDiagramWatchlistButton({token}: { token: string }) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
useEffect(() => {
setEdges([])
@@ -30,15 +29,16 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
setNodes(e.nodes)
setEdges(e.edges)
}).catch(() => setOpen(false)).finally(() => setLoading(false))
}, [open])
return <>
return (
<>
<Typography.Link>
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
<ApartmentOutlined
title={t`View the Watchlist Entity Diagram`}
style={{color: 'darkviolet'}}
onClick={() => setOpen(true)}/>
onClick={() => setOpen(true)}
/>
</Typography.Link>
<Modal
title={t`Watchlist Entity Diagram`}
@@ -47,7 +47,7 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
loading={loading}
footer={
<Space>
<Button type="default" onClick={() => setOpen(false)}>
<Button type='default' onClick={() => setOpen(false)}>
Close
</Button>
</Space>
@@ -78,4 +78,5 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
</Flex>
</Modal>
</>
)
}

View File

@@ -1,38 +1,39 @@
import dagre from "dagre"
import dagre from 'dagre'
import {Edge, Node, Position} from '@xyflow/react'
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
export const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodeWidth = 172
const nodeHeight = 200
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({rankdir: direction});
const isHorizontal = direction === 'LR'
dagreGraph.setGraph({rankdir: direction})
nodes.forEach((node: any) => {
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
});
nodes.forEach(node => {
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight})
})
edges.forEach((edge: any) => {
dagreGraph.setEdge(edge.source, edge.target);
});
edges.forEach(edge => {
dagreGraph.setEdge(edge.source, edge.target)
})
dagre.layout(dagreGraph);
dagre.layout(dagreGraph)
const newNodes = nodes.map((node: any) => {
const newNodes: Node[] = nodes.map(node => {
const nodeWithPosition = dagreGraph.node(node.id)
return {
...node,
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
},
};
});
return {nodes: newNodes, edges};
}
}
})
return {nodes: newNodes, edges}
}

View File

@@ -1,17 +1,18 @@
import {Domain, Watchlist} from "../../../../utils/api";
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
import {t} from "ttag";
import {Domain, Watchlist} from '../../../../utils/api'
import {rdapRoleTranslation} from '../../../../utils/functions/rdapTranslation'
import {t} from 'ttag'
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
import {rolesToColor} from '../../../../utils/functions/rolesToColor'
import {Edge} from '@xyflow/react'
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
export function domainEntitiesToEdges(d: Domain, withRegistrar = false): Edge[] {
const rdapRoleTranslated = rdapRoleTranslation()
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
return d.entities
.filter(e =>
!e.deleted &&
(withRegistrar || !e.roles.includes('registrar')) &&
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
)
.map(e => ({
id: `e-${d.ldhName}-${e.entity.handle}`,
@@ -21,11 +22,11 @@ export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
label: e.roles
.map(r => rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r)
.join(', '),
animated: e.roles.includes('registrant'),
animated: e.roles.includes('registrant')
}))
}
export const domainNSToEdges = (d: Domain) => d.nameservers
export const domainNSToEdges = (d: Domain): Edge[] => d.nameservers
.map(ns => ({
id: `ns-${d.ldhName}-${ns.ldhName}`,
source: d.ldhName,
@@ -34,7 +35,7 @@ export const domainNSToEdges = (d: Domain) => d.nameservers
label: 'DNS'
}))
export const tldToEdge = (d: Domain) => ({
export const tldToEdge = (d: Domain): Edge => ({
id: `tld-${d.ldhName}-${d.tld.tld}`,
source: d.tld.tld,
target: d.ldhName,
@@ -42,7 +43,7 @@ export const tldToEdge = (d: Domain) => ({
label: t`Registry`
})
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false): Edge[] {
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
const tldEdge = watchlist.domains.map(tldToEdge)

View File

@@ -1,28 +1,31 @@
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
import React from "react";
import {Domain, Nameserver, Tld, Watchlist} from '../../../../utils/api'
import React from 'react'
import {t} from 'ttag'
import {entityToName} from "../../../../utils/functions/entityToName";
import {entityToName} from '../../../../utils/functions/entityToName'
import {Node} from '@xyflow/react'
export const domainToNode = (d: Domain) => ({
export const domainToNode = (d: Domain): Node => ({
id: d.ldhName,
position: {x: 0, y: 0},
data: {label: <b>{d.ldhName}</b>},
style: {
width: 200
}
})
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
export const domainEntitiesToNode = (d: Domain, withRegistrar = false): Node[] => {
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
return d.entities
.filter(e =>
!e.deleted &&
(withRegistrar || !e.roles.includes('registrar')) &&
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
)
.map(e => {
return {
id: e.entity.handle,
position: {x: 0, y: 0},
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
data: {label: entityToName(e)},
style: {
@@ -32,8 +35,9 @@ export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
})
}
export const tldToNode = (tld: Tld) => ({
export const tldToNode = (tld: Tld): Node => ({
id: tld.tld,
position: {x: 0, y: 0},
data: {label: t`.${tld.tld} Registry`},
type: 'input',
style: {
@@ -41,8 +45,9 @@ export const tldToNode = (tld: Tld) => ({
}
})
export const nsToNode = (ns: Nameserver) => ({
export const nsToNode = (ns: Nameserver): Node => ({
id: ns.ldhName,
position: {x: 0, y: 0},
data: {label: ns.ldhName},
type: 'output',
style: {
@@ -50,8 +55,7 @@ export const nsToNode = (ns: Nameserver) => ({
}
})
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false) {
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false): Node[] {
const domains = watchlist.domains.map(domainToNode)
const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())]
const tlds = [...new Set(watchlist.domains.map(d => d.tld))].filter(t => t.tld !== '.').map(tldToNode)

View File

@@ -1,4 +1,4 @@
declare module "*.md" {
const content: string;
export default content;
declare module '*.md' {
const content: string
export default content
}

View File

@@ -1,13 +1,13 @@
import {Breakpoint, theme} from 'antd';
import {useMediaQuery} from 'react-responsive';
import {Breakpoint, theme} from 'antd'
import {useMediaQuery} from 'react-responsive'
const {useToken} = theme;
const {useToken} = theme
type ScreenProperty = 'screenXXL' | 'screenXL' | 'screenLG' | 'screenMD' | 'screenSM' | 'screenXS';
type ScreenProperty = 'screenXXL' | 'screenXL' | 'screenLG' | 'screenMD' | 'screenSM' | 'screenXS'
const propertyName = (breakpoint: Breakpoint): ScreenProperty => {
return 'screen' + breakpoint.toUpperCase() as ScreenProperty
};
}
export default function useBreakpoint(
breakpoint: Breakpoint

View File

@@ -1,23 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import {HashRouter} from "react-router-dom";
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import {HashRouter} from 'react-router-dom'
import 'antd/dist/reset.css';
import 'antd/dist/reset.css'
import './i18n'
import './index.css'
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
function Index() {
return (
<HashRouter>
<App/>
</HashRouter>
);
)
}
root.render(<Index/>)

View File

@@ -1,16 +1,24 @@
import React, {createContext, useEffect, useState} from "react";
import {Button, Card} from "antd";
import React, {createContext, useEffect, useState} from 'react'
import {Button, Card} from 'antd'
import {t} from 'ttag'
import TextPage from "./TextPage";
import {LoginForm} from "../components/LoginForm";
import {getConfiguration, InstanceConfig} from "../utils/api";
import {RegisterForm} from "../components/RegisterForm";
import TextPage from './TextPage'
import {LoginForm} from '../components/LoginForm'
import {getConfiguration, InstanceConfig} from '../utils/api'
import {RegisterForm} from '../components/RegisterForm'
export const AuthenticatedContext = createContext<any>(null)
export const AuthenticatedContext = createContext<
{
authenticated: (authenticated: boolean) => void
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
}
>({
authenticated: () => {
},
setIsAuthenticated: () => {
}
})
export default function LoginPage() {
const [wantRegister, setWantRegister] = useState<boolean>(false)
const [configuration, setConfiguration] = useState<InstanceConfig>()
@@ -22,19 +30,24 @@ export default function LoginPage() {
getConfiguration().then(setConfiguration)
}, [])
return <Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
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'
<Button
type='link'
block
style={{marginTop: '1em'}}
onClick={toggleWantRegister}>{wantRegister ? t`Log in` : t`Create an account`}</Button>
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>
</Card>
)
}

View File

@@ -1,12 +1,13 @@
import {Result} from "antd";
import React from "react";
import {Result} from 'antd'
import React from 'react'
import {t} from 'ttag'
export default function NotFoundPage() {
return <Result
status="404"
title="404"
return (
<Result
status='404'
title='404'
subTitle={t`Sorry, the page you visited does not exist.`}
/>
)
}

View File

@@ -1,17 +1,16 @@
import React, {useEffect, useState} from "react";
import {getStatistics, Statistics} from "../utils/api";
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd";
import {t} from "ttag";
import React, {useEffect, useState} from 'react'
import {getStatistics, Statistics} from '../utils/api'
import {Card, Col, Divider, Row, Statistic, Tooltip} from 'antd'
import {t} from 'ttag'
import {
AimOutlined,
CompassOutlined,
DatabaseOutlined,
FieldTimeOutlined,
NotificationOutlined
} from "@ant-design/icons";
} from '@ant-design/icons'
export default function StatisticsPage() {
const [stats, setStats] = useState<Statistics>()
useEffect(() => {
@@ -20,11 +19,12 @@ export default function StatisticsPage() {
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
const successRate = stats !== undefined ?
(totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
const successRate = stats !== undefined
? (totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
: undefined
return <>
return (
<>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
@@ -89,7 +89,8 @@ export default function StatisticsPage() {
<Col span={12}>
<Card bordered={false}>
<Tooltip
title={t`This value is based on the status code of the HTTP response from the providers following the domain order.`}>
title={t`This value is based on the status code of the HTTP response from the providers following the domain order.`}
>
<Statistic
loading={stats === undefined}
title={t`Success rate`}
@@ -105,7 +106,7 @@ export default function StatisticsPage() {
<Row gutter={16} justify='center' align='middle'>
{stats?.domainCount
.sort((a, b) => b.domain - a.domain)
.map(({domain, tld}) => <Col span={4}>
.map(({domain, tld}) => <Col key={tld} span={4}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
@@ -117,4 +118,5 @@ export default function StatisticsPage() {
</Col>)}
</Row>
</>
)
}

View File

@@ -1,8 +1,8 @@
import React, {useEffect, useState} from "react";
import snarkdown from "snarkdown"
import {Skeleton, Typography} from "antd";
import axios from "axios";
import {t} from "ttag";
import React, {useEffect, useState} from 'react'
import snarkdown from 'snarkdown'
import {Skeleton, Typography} from 'antd'
import axios from 'axios'
import {t} from 'ttag'
export default function TextPage({resource}: { resource: string }) {
const [loading, setLoading] = useState<boolean>(false)
@@ -12,18 +12,22 @@ export default function TextPage({resource}: { resource: string }) {
setLoading(true)
axios.get('/content/' + resource)
.then(res => setMarkdown(res.data))
.catch(err => {
.catch(() => {
console.error(`Please create the /public/content/${resource} file.`)
setMarkdown(undefined)
})
.finally(() => setLoading(false))
}, [resource])
return <Skeleton loading={loading} active>
{markdown !== undefined ? <div
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div> :
<Typography.Text strong>
return (
<Skeleton loading={loading} active>
{markdown !== undefined
? <div
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}
/>
: <Typography.Text strong>
{t`📝 Please create the /public/content/${resource} file.`}
</Typography.Text>}
</Skeleton>
)
}

View File

@@ -1,18 +1,18 @@
import React, {useEffect, useState} from "react";
import {Card, Flex, Skeleton, Typography} from "antd";
import {getUser, User} from "../utils/api";
import React, {useEffect, useState} from 'react'
import {Card, Flex, Skeleton, Typography} from 'antd'
import {getUser, User} from '../utils/api'
import {t} from 'ttag'
export default function UserPage() {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
getUser().then(setUser)
}, [])
return <Skeleton loading={user === null} active>
<Flex gap="middle" align="center" justify="center" vertical>
return (
<Skeleton loading={user === null} active>
<Flex gap='middle' align='center' justify='center' vertical>
<Card title={t`My Account`}>
<Typography.Paragraph>
{t`Username`} : {user?.email}
@@ -23,4 +23,5 @@ export default function UserPage() {
</Card>
</Flex>
</Skeleton>
)
}

View File

@@ -1,12 +1,12 @@
import React, {useEffect, useState} from "react";
import {Empty, Flex, FormProps, message, Skeleton} from "antd";
import {Domain, getDomain} from "../../utils/api";
import {AxiosError} from "axios"
import React, {useEffect, useState} from 'react'
import {Empty, Flex, FormProps, message, Skeleton} from 'antd'
import {Domain, getDomain} from '../../utils/api'
import {AxiosError} from 'axios'
import {t} from 'ttag'
import {DomainSearchBar, FieldType} from "../../components/search/DomainSearchBar";
import {DomainResult} from "../../components/search/DomainResult";
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
import {useNavigate, useParams} from "react-router-dom";
import {DomainSearchBar, FieldType} from '../../components/search/DomainSearchBar'
import {DomainResult} from '../../components/search/DomainResult'
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
import {useNavigate, useParams} from 'react-router-dom'
export default function DomainSearchPage() {
const {query} = useParams()
@@ -36,17 +36,21 @@ export default function DomainSearchPage() {
onFinish({ldhName: query})
}, [])
return <Flex gap="middle" align="center" justify="center" vertical>
return (
<Flex gap='middle' align='center' justify='center' vertical>
{contextHolder}
<DomainSearchBar initialValue={query} onFinish={onFinish}/>
<Skeleton loading={domain === null} active>
{
domain &&
(!domain.deleted ? <DomainResult domain={domain}/>
(domain != null) &&
(!domain.deleted
? <DomainResult domain={domain}/>
: <Empty
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}
/>)
}
</Skeleton>
</Flex>
)
}

View File

@@ -1,7 +1,9 @@
import React from "react";
import React from 'react'
export default function EntitySearchPage() {
return <p>
return (
<p>
Not implemented
</p>
)
}

View File

@@ -1,7 +1,9 @@
import React from "react";
import React from 'react'
export default function NameserverSearchPage() {
return <p>
return (
<p>
Not implemented
</p>
)
}

View File

@@ -1,41 +1,53 @@
import React, {useEffect, useState} from "react";
import {Collapse, Divider, Table, Typography} from "antd";
import {getTldList, Tld} from "../../utils/api";
import React, {ReactElement, useEffect, useState} from 'react'
import {Collapse, Divider, Table, Typography} from 'antd'
import {getTldList, Tld} from '../../utils/api'
import {t} from 'ttag'
import {regionNames} from "../../i18n";
import useBreakpoint from "../../hooks/useBreakpoint";
import {ColumnType} from "antd/es/table";
import punycode from "punycode/punycode";
import {getCountryCode} from "../../utils/functions/getCountryCode";
import {tldToEmoji} from "../../utils/functions/tldToEmoji";
import {regionNames} from '../../i18n'
import useBreakpoint from '../../hooks/useBreakpoint'
import {ColumnType} from 'antd/es/table'
import punycode from 'punycode/punycode'
import {getCountryCode} from '../../utils/functions/getCountryCode'
import {tldToEmoji} from '../../utils/functions/tldToEmoji'
const {Text, Paragraph} = Typography
type TldType = 'iTLD' | 'sTLD' | 'gTLD' | 'ccTLD'
type FiltersType = { type: TldType, contractTerminated?: boolean, specification13?: boolean }
interface FiltersType {
type: TldType,
contractTerminated?: boolean,
specification13?: boolean
}
function TldTable(filters: FiltersType) {
interface TableRow {
key: string
TLD: ReactElement
Flag?: string
Country?: string
}
const sm = useBreakpoint('sm')
const [dataTable, setDataTable] = useState<Tld[]>([])
const [dataTable, setDataTable] = useState<TableRow[]>([])
const [total, setTotal] = useState(0)
const fetchData = (params: FiltersType & { page: number, itemsPerPage: number }) => {
getTldList(params).then((data) => {
setTotal(data['hydra:totalItems'])
setDataTable(data['hydra:member'].map((tld: Tld) => {
const rowData = {
key: tld.tld,
TLD: <Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text>
}
switch (filters.type) {
case 'ccTLD':
const type = filters.type
let countryName
switch (type) {
case 'ccTLD':
try {
countryName = regionNames.of(getCountryCode(tld.tld))
} catch (e) {
} catch {
countryName = '-'
}
@@ -60,28 +72,32 @@ function TldTable(filters: FiltersType) {
fetchData({...filters, page: 1, itemsPerPage: 30})
}, [])
let columns: ColumnType<any>[] = [
let columns: Array<ColumnType<TableRow>> = [
{
title: t`TLD`,
dataIndex: "TLD"
dataIndex: 'TLD'
}
]
if (filters.type === 'ccTLD') columns = [...columns, {
if (filters.type === 'ccTLD') {
columns = [...columns, {
title: t`Flag`,
dataIndex: "Flag",
dataIndex: 'Flag'
}, {
title: t`Country`,
dataIndex: "Country"
dataIndex: 'Country'
}]
}
if (filters.type === 'gTLD') columns = [...columns, {
if (filters.type === 'gTLD') {
columns = [...columns, {
title: t`Registry Operator`,
dataIndex: "Operator"
dataIndex: 'Operator'
}]
}
return <Table
return (
<Table
columns={columns}
dataSource={dataTable}
pagination={{
@@ -95,13 +111,14 @@ function TldTable(filters: FiltersType) {
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
)
}
export default function TldPage() {
const sm = useBreakpoint('sm')
return <>
return (
<>
<Paragraph>
{t`This page presents all active TLDs in the root zone database.`}
</Paragraph>
@@ -140,7 +157,7 @@ export default function TldPage() {
children: <>
<Text>{t`Generic top-level domains associated with specific brands, allowing companies to use their own brand names as domains.`}</Text>
<Divider/>
<TldTable type='gTLD' contractTerminated={false} specification13={true}/>
<TldTable type='gTLD' contractTerminated={false} specification13/>
</>
},
{
@@ -154,4 +171,5 @@ export default function TldPage() {
]}
/>
</>
)
}

View File

@@ -1,12 +1,12 @@
import React, {useEffect, useState} from "react";
import {Card, Flex, Form, message, Skeleton} from "antd";
import {t} from "ttag";
import {Connector, getConnectors, postConnector} from "../../utils/api/connectors";
import {ConnectorForm} from "../../components/tracking/connector/ConnectorForm";
import {AxiosError} from "axios";
import {ConnectorElement, ConnectorsList} from "../../components/tracking/connector/ConnectorsList";
import React, {useEffect, useState} from 'react'
import {Card, Flex, Form, message, Skeleton} from 'antd'
import {t} from 'ttag'
import {Connector, getConnectors, postConnector} from '../../utils/api/connectors'
import {ConnectorForm} from '../../components/tracking/connector/ConnectorForm'
import {AxiosError} from 'axios'
import {ConnectorElement, ConnectorsList} from '../../components/tracking/connector/ConnectorsList'
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
export default function ConnectorPage() {
const [form] = Form.useForm()
@@ -14,7 +14,7 @@ export default function ConnectorPage() {
const [connectors, setConnectors] = useState<ConnectorElement[] | null>()
const onCreateConnector = (values: Connector) => {
postConnector(values).then((w) => {
postConnector(values).then(() => {
form.resetFields()
refreshConnectors()
messageApi.success(t`Connector created !`)
@@ -23,7 +23,7 @@ export default function ConnectorPage() {
})
}
const refreshConnectors = () => getConnectors().then(c => {
const refreshConnectors = async () => await getConnectors().then(c => {
setConnectors(c['hydra:member'])
}).catch((e: AxiosError) => {
setConnectors(undefined)
@@ -34,18 +34,17 @@ export default function ConnectorPage() {
refreshConnectors()
}, [])
return <Flex gap="middle" align="center" justify="center" vertical>
return (
<Flex gap='middle' align='center' justify='center' vertical>
<Card title={t`Create a Connector`} style={{width: '100%'}}>
{contextHolder}
<ConnectorForm form={form} onCreate={onCreateConnector}/>
</Card>
<Skeleton loading={connectors === undefined} active>
{connectors && connectors.length > 0 &&
<ConnectorsList connectors={connectors} onDelete={refreshConnectors}/>
}
{(connectors != null) && connectors.length > 0 &&
<ConnectorsList connectors={connectors} onDelete={refreshConnectors}/>}
</Skeleton>
</Flex>
)
}

View File

@@ -1,5 +1,5 @@
import {TrackedDomainTable} from "../../components/tracking/watchlist/TrackedDomainTable";
import React from "react";
import {TrackedDomainTable} from '../../components/tracking/watchlist/TrackedDomainTable'
import React from 'react'
export default function TrackedDomainPage() {
return <TrackedDomainTable/>

View File

@@ -1,34 +1,19 @@
import React, {useEffect, useState} from "react";
import {Card, Divider, Flex, Form, message} from "antd";
import {EventAction, getWatchlists, postWatchlist, putWatchlist} from "../../utils/api";
import {AxiosError} from "axios";
import React, {useEffect, useState} from 'react'
import {Card, Divider, Flex, Form, message} from 'antd'
import {getWatchlists, postWatchlist, putWatchlist, Watchlist} from '../../utils/api'
import {AxiosError} from 'axios'
import {t} from 'ttag'
import {WatchlistForm} from "../../components/tracking/watchlist/WatchlistForm";
import {WatchlistsList} from "../../components/tracking/watchlist/WatchlistsList";
import {Connector, getConnectors} from "../../utils/api/connectors";
import {WatchlistForm} from '../../components/tracking/watchlist/WatchlistForm'
import {WatchlistsList} from '../../components/tracking/watchlist/WatchlistsList'
import {Connector, getConnectors} from '../../utils/api/connectors'
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
export type Watchlist = {
interface FormValuesType {
name?: string
token: string,
domains: { ldhName: string, deleted: boolean, status: string[] }[],
triggers?: { event: EventAction, action: string }[],
dsn?: string[]
connector?: {
id: string
provider: string
createdAt: string
}
createdAt: string
}
type FormValuesType = {
name?: string
domains: string[],
domains: string[]
triggers: string[]
connector?: string,
connector?: string
dsn?: string[]
}
@@ -52,14 +37,13 @@ const getRequestDataFromForm = (values: FormValuesType) => {
}
export default function WatchlistPage() {
const [form] = Form.useForm()
const [messageApi, contextHolder] = message.useMessage()
const [watchlists, setWatchlists] = useState<Watchlist[]>()
const [connectors, setConnectors] = useState<(Connector & { id: string })[]>()
const [connectors, setConnectors] = useState<Array<Connector & { id: string }>>()
const onCreateWatchlist = (values: FormValuesType) => {
postWatchlist(getRequestDataFromForm(values)).then((w) => {
postWatchlist(getRequestDataFromForm(values)).then(() => {
form.resetFields()
refreshWatchlists()
messageApi.success(t`Watchlist created !`)
@@ -68,18 +52,18 @@ export default function WatchlistPage() {
})
}
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => putWatchlist({
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => await putWatchlist({
token: values.token,
...getRequestDataFromForm(values)
}
).then((w) => {
).then(() => {
refreshWatchlists()
messageApi.success(t`Watchlist updated !`)
}).catch((e: AxiosError) => {
throw showErrorAPI(e, messageApi)
})
const refreshWatchlists = () => getWatchlists().then(w => {
const refreshWatchlists = async () => await getWatchlists().then(w => {
setWatchlists(w['hydra:member'])
}).catch((e: AxiosError) => {
setWatchlists(undefined)
@@ -95,18 +79,20 @@ export default function WatchlistPage() {
})
}, [])
return <Flex gap="middle" align="center" justify="center" vertical>
return (
<Flex gap='middle' align='center' justify='center' vertical>
{contextHolder}
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
{connectors &&
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation={true}/>
}
{(connectors != null) &&
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation/>}
</Card>
<Divider/>
{connectors && watchlists && watchlists.length > 0 &&
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}
{(connectors != null) && (watchlists != null) && watchlists.length > 0 &&
<WatchlistsList
watchlists={watchlists} onDelete={refreshWatchlists}
connectors={connectors}
onUpdateWatchlist={onUpdateWatchlist}
/>}
</Flex>
)
}

View File

@@ -1,4 +1,5 @@
import {request} from "./index";
import {request} from './index'
import {ConnectorElement} from '../../components/tracking/connector/ConnectorsList'
export enum ConnectorProvider {
OVH = 'ovh',
@@ -7,13 +8,18 @@ export enum ConnectorProvider {
NAMECHEAP = 'namecheap'
}
export type Connector = {
export interface Connector {
provider: ConnectorProvider
authData: object
}
export async function getConnectors() {
const response = await request({
interface ConnectorResponse {
'hydra:totalItems': number
'hydra:member': ConnectorElement[]
}
export async function getConnectors(): Promise<ConnectorResponse> {
const response = await request<ConnectorResponse>({
url: 'connectors'
})
return response.data
@@ -25,7 +31,7 @@ export async function postConnector(connector: Connector) {
url: 'connectors',
data: connector,
headers: {
"Content-Type": 'application/json'
'Content-Type': 'application/json'
}
})
return response.data

View File

@@ -1,5 +1,4 @@
import {Domain, request} from ".";
import {Domain, request} from '.'
export async function getDomain(ldhName: string): Promise<Domain> {
const response = await request<Domain>({

View File

@@ -1,5 +1,4 @@
import axios, {AxiosRequestConfig, AxiosResponse} from "axios";
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'
export type EventAction =
'registration'
@@ -26,7 +25,12 @@ export interface Event {
export interface Entity {
handle: string
jCard: any
jCard: ['vcard', Array<[
string,
{ [key: string]: string | string[] },
string,
string | string[],
]>] | []
}
export interface Nameserver {
@@ -50,12 +54,12 @@ export interface Domain {
handle: string
status: string[]
events: Event[]
entities: {
entities: Array<{
entity: Entity
events: Event[]
roles: string[]
deleted: boolean
}[]
}>
nameservers: Nameserver[]
tld: Tld
deleted: boolean
@@ -70,20 +74,24 @@ export interface User {
export interface WatchlistRequest {
name?: string
domains: string[],
triggers: { event: EventAction, action: TriggerAction }[],
domains: string[]
triggers: Array<{ event: EventAction, action: TriggerAction }>
connector?: string
dsn?: string[]
}
export interface Watchlist {
token: string
name?: string
domains: Domain[],
triggers: { event: EventAction, action: TriggerAction }[],
connector?: string
createdAt: string
token: string
domains: Domain[]
triggers?: Array<{ event: EventAction, action: string }>
dsn?: string[]
connector?: {
id: string
provider: string
createdAt: string
}
createdAt: string
}
export interface InstanceConfig {
@@ -97,28 +105,30 @@ export interface Statistics {
alertSent: number
domainPurchased: number
domainPurchaseFailed: number
domainCount: {tld: string, domain: number}[]
domainCount: Array<{ tld: string, domain: number }>
domainCountTotal: number
domainTracked: number
}
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
export interface TrackedDomains {
'hydra:totalItems': number
'hydra:member': Domain[]
}
export async function request<T = object, R = AxiosResponse<T>, D = object>(config: AxiosRequestConfig): Promise<R> {
const axiosConfig: AxiosRequestConfig = {
...config,
baseURL: '/api',
withCredentials: true,
headers: {
Accept: 'application/ld+json',
...config.headers,
...config.headers
}
}
return await axios.request<T, R, D>(axiosConfig)
}
export * from './domain'
export * from './tld'
export * from './user'
export * from './watchlist'

View File

@@ -1,15 +1,13 @@
import {request} from "./index";
import {request, Tld} from './index'
interface Tld {
tld: string
contractTerminated: boolean
registryOperator: string
specification13: boolean
interface TldList {
'hydra:totalItems': number
'hydra:member': Tld[]
}
export async function getTldList(params: object): Promise<any> {
return (await request<Tld[]>({
export async function getTldList(params: object): Promise<TldList> {
return (await request<TldList>({
url: 'tld',
params,
params
})).data
}

View File

@@ -1,5 +1,4 @@
import {InstanceConfig, request, Statistics, User} from "./index";
import {InstanceConfig, request, Statistics, User} from './index'
export async function login(email: string, password: string): Promise<boolean> {
const response = await request({
@@ -19,7 +18,6 @@ export async function register(email: string, password: string): Promise<boolean
return response.status === 201
}
export async function getUser(): Promise<User> {
const response = await request<User>({
url: 'me'

View File

@@ -1,7 +1,12 @@
import {Domain, request, Watchlist, WatchlistRequest} from "./index";
import {request, TrackedDomains, Watchlist, WatchlistRequest} from './index'
export async function getWatchlists() {
const response = await request({
interface WatchlistList {
'hydra:totalItems': number
'hydra:member': Watchlist[]
}
export async function getWatchlists(): Promise<WatchlistList> {
const response = await request<WatchlistList>({
url: 'watchlists'
})
return response.data
@@ -20,7 +25,7 @@ export async function postWatchlist(watchlist: WatchlistRequest) {
url: 'watchlists',
data: watchlist,
headers: {
"Content-Type": 'application/json'
'Content-Type': 'application/json'
}
})
return response.data
@@ -37,17 +42,16 @@ export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { toke
const response = await request<WatchlistRequest>({
method: 'PUT',
url: 'watchlists/' + watchlist.token,
data: watchlist,
data: watchlist
})
return response.data
}
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<any> {
const response = await request({
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<TrackedDomains> {
const response = await request<TrackedDomains>({
method: 'GET',
url: 'tracked',
params
})
return response.data
}

View File

@@ -1,11 +1,19 @@
import {EventAction} from "../api";
import {EventAction} from '../api'
export const actionToColor = (a: EventAction) => a === 'registration' ? 'green' :
a === 'reregistration' ? 'cyan' :
a === 'expiration' ? 'red' :
a === 'deletion' ? 'magenta' :
a === 'transfer' ? 'orange' :
a === 'last changed' ? 'blue' :
a === 'registrar expiration' ? 'red' :
a === 'reinstantiation' ? 'purple' :
a === 'enum validation expiration' ? 'red' : 'default'
export const actionToColor = (a: EventAction) => a === 'registration'
? 'green'
: a === 'reregistration'
? 'cyan'
: a === 'expiration'
? 'red'
: a === 'deletion'
? 'magenta'
: a === 'transfer'
? 'orange'
: a === 'last changed'
? 'blue'
: a === 'registrar expiration'
? 'red'
: a === 'reinstantiation'
? 'purple'
: a === 'enum validation expiration' ? 'red' : 'default'

View File

@@ -1,4 +1,4 @@
import {EventAction} from "../api";
import {EventAction} from '../api'
import {
ClockCircleOutlined,
DeleteOutlined,
@@ -9,20 +9,31 @@ import {
SignatureOutlined,
SyncOutlined,
UnlockOutlined
} from "@ant-design/icons";
import React from "react";
} from '@ant-design/icons'
import React from 'react'
export const actionToIcon = (a: EventAction) => a === 'registration' ?
<SignatureOutlined style={{fontSize: '16px'}}/> : a === 'expiration' ?
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'transfer' ?
<ShareAltOutlined style={{fontSize: '16px'}}/> : a === 'last changed' ?
<SyncOutlined style={{fontSize: '16px'}}/> : a === 'deletion' ?
<DeleteOutlined style={{fontSize: '16px'}}/> : a === 'reregistration' ?
<ReloadOutlined style={{fontSize: '16px'}}/> : a === 'locked' ?
<LockOutlined style={{fontSize: '16px'}}/> : a === 'unlocked' ?
<UnlockOutlined style={{fontSize: '16px'}}/> : a === 'registrar expiration' ?
<ClockCircleOutlined
style={{fontSize: '16px'}}/> : a === 'enum validation expiration' ?
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'reinstantiation' ?
<ReloadOutlined style={{fontSize: '16px'}}/> :
<PushpinOutlined style={{fontSize: '16px'}}/>
export const actionToIcon = (a: EventAction) => a === 'registration'
? <SignatureOutlined style={{fontSize: '16px'}}/>
: a === 'expiration'
? <ClockCircleOutlined style={{fontSize: '16px'}}/>
: a === 'transfer'
? <ShareAltOutlined style={{fontSize: '16px'}}/>
: a === 'last changed'
? <SyncOutlined style={{fontSize: '16px'}}/>
: a === 'deletion'
? <DeleteOutlined style={{fontSize: '16px'}}/>
: a === 'reregistration'
? <ReloadOutlined style={{fontSize: '16px'}}/>
: a === 'locked'
? <LockOutlined style={{fontSize: '16px'}}/>
: a === 'unlocked'
? <UnlockOutlined style={{fontSize: '16px'}}/>
: a === 'registrar expiration'
? <ClockCircleOutlined
style={{fontSize: '16px'}}
/>
: a === 'enum validation expiration'
? <ClockCircleOutlined style={{fontSize: '16px'}}/>
: a === 'reinstantiation'
? <ReloadOutlined style={{fontSize: '16px'}}/>
: <PushpinOutlined style={{fontSize: '16px'}}/>

View File

@@ -1,5 +1,5 @@
import {Entity} from "../api";
import vCard from "vcf";
import {Entity} from '../api'
import vCard from 'vcf'
export const entityToName = (e: { entity: Entity }): string => {
if (e.entity.jCard.length === 0) return e.entity.handle

View File

@@ -1,5 +1,8 @@
export const eppStatusCodeToColor = (s: string) =>
['active', 'ok'].includes(s) ? 'green' :
['pending delete', 'redemption period'].includes(s) ? 'red' :
s.startsWith('client') ? 'purple' :
s.startsWith('server') ? 'geekblue' : 'blue'
['active', 'ok'].includes(s)
? 'green'
: ['pending delete', 'redemption period'].includes(s)
? 'red'
: s.startsWith('client')
? 'purple'
: s.startsWith('server') ? 'geekblue' : 'blue'

View File

@@ -1,9 +1,9 @@
import vCard from "vcf";
import {Entity} from "../api";
import vCard from 'vcf'
import {Entity} from '../api'
export const extractDetailsFromJCard = (e: { entity: Entity }): {
fn?: string
organization?: string;
organization?: string
} => {
if (e.entity.jCard.length === 0) return {fn: e.entity.handle}
const jCard = vCard.fromJSON(e.entity.jCard)

View File

@@ -1,4 +1,4 @@
import {t} from "ttag";
import {t} from 'ttag'
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
@@ -17,7 +17,6 @@ export const rdapRoleTranslation = () => ({
noc: t`Noc`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
@@ -35,7 +34,6 @@ export const rdapRoleDetailTranslation = () => ({
noc: t`The entity object instance handles communications related to a network operations center (NOC).`
})
/**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
*/
@@ -75,20 +73,20 @@ export const rdapEventDetailTranslation = () => ({
* @see https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
*/
export const rdapStatusCodeDetailTranslation = () => ({
'validated': t`Signifies that the data of the object instance has been found to be accurate.`,
validated: t`Signifies that the data of the object instance has been found to be accurate.`,
'renew prohibited': t`Renewal or reregistration of the object instance is forbidden.`,
'update prohibited': t`Updates to the object instance are forbidden.`,
'transfer prohibited': t`Transfers of the registration from one registrar to another are forbidden.`,
'delete prohibited': t`Deletion of the registration of the object instance is forbidden.`,
'proxy': t`The registration of the object instance has been performed by a third party.`,
'private': t`The information of the object instance is not designated for public consumption.`,
'removed': t`Some of the information of the object instance has not been made available and has been removed.`,
'obscured': t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
'associated': t`The object instance is associated with other object instances in the registry.`,
'locked': t`Changes to the object instance cannot be made, including the association of other object instances.`,
proxy: t`The registration of the object instance has been performed by a third party.`,
private: t`The information of the object instance is not designated for public consumption.`,
removed: t`Some of the information of the object instance has not been made available and has been removed.`,
obscured: t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
associated: t`The object instance is associated with other object instances in the registry.`,
locked: t`Changes to the object instance cannot be made, including the association of other object instances.`,
'active': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
'inactive': t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
active: t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
inactive: t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
'pending create': t`This status code indicates that a request to create your domain has been received and is being processed.`,
'pending renew': t`This status code indicates that a request to renew your domain has been received and is being processed.`,
'pending transfer': t`This status code indicates that a request to transfer your domain to a new registrar has been received and is being processed.`,
@@ -96,7 +94,7 @@ export const rdapStatusCodeDetailTranslation = () => ({
'pending delete': t`This status code may be mixed with redemptionPeriod or pendingRestore. In such case, depending on the status (i.e. redemptionPeriod or pendingRestore) set in the domain name, the corresponding description presented above applies. If this status is not combined with the redemptionPeriod or pendingRestore status, the pendingDelete status code indicates that your domain has been in redemptionPeriod status for 30 days and you have not restored it within that 30-day period. Your domain will remain in this status for several days, after which time your domain will be purged and dropped from the registry database. Once deletion occurs, the domain is available for re-registration in accordance with the registry's policies.`,
'add period': t`This grace period is provided after the initial registration of a domain name. If the registrar deletes the domain name during this period, the registry may provide credit to the registrar for the cost of the registration.`,
'auto renew period': t`This grace period is provided after a domain name registration period expires and is extended (renewed) automatically by the registry. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
'ok': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
ok: t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
'client delete prohibited': t`This status code tells your domain's registry to reject requests to delete the domain.`,
'client hold': t`This status code tells your domain's registry to not activate your domain in the DNS and as a consequence, it will not resolve. It is an uncommon status that is usually enacted during legal disputes, non-payment, or when your domain is subject to deletion.`,
'client renew prohibited': t`This status code tells your domain's registry to reject requests to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
@@ -112,6 +110,6 @@ export const rdapStatusCodeDetailTranslation = () => ({
'server hold': t`This status code is set by your domain's Registry Operator. Your domain is not activated in the DNS.`,
'transfer period': t`This grace period is provided after the successful transfer of a domain name from one registrar to another. If the new registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the transfer.`,
'administrative': t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
'reserved': t`The object instance has been allocated to an IANA special-purpose address registry.`,
administrative: t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
reserved: t`The object instance has been allocated to an IANA special-purpose address registry.`
})

View File

@@ -1,4 +1,4 @@
import {Avatar} from "antd";
import {Avatar} from 'antd'
import {
BankOutlined,
DollarOutlined,
@@ -6,20 +6,22 @@ import {
SignatureOutlined,
ToolOutlined,
UserOutlined
} from "@ant-design/icons";
import React from "react";
} from '@ant-design/icons'
import React from 'react'
import {rolesToColor} from "./rolesToColor";
import {rolesToColor} from './rolesToColor'
export const roleToAvatar = (e: { roles: string[] }) => <Avatar style={{backgroundColor: rolesToColor(e.roles)}}
icon={e.roles.includes('registrant') ?
<SignatureOutlined/> :
e.roles.includes('registrar') ?
<BankOutlined/> :
e.roles.includes('administrative') ?
<IdcardOutlined/> :
e.roles.includes('technical') ?
<ToolOutlined/> :
e.roles.includes('billing') ?
<DollarOutlined/> :
<UserOutlined/>}/>
export const roleToAvatar = (e: { roles: string[] }) => <Avatar
style={{backgroundColor: rolesToColor(e.roles)}}
icon={e.roles.includes('registrant')
? <SignatureOutlined/>
: e.roles.includes('registrar')
? <BankOutlined/>
: e.roles.includes('administrative')
? <IdcardOutlined/>
: e.roles.includes('technical')
? <ToolOutlined/>
: e.roles.includes('billing')
? <DollarOutlined/>
: <UserOutlined/>}
/>

View File

@@ -1,6 +1,11 @@
export const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' :
roles.includes('registrar') ? 'purple' :
roles.includes('administrative') ? 'blue' :
roles.includes('technical') ? 'orange' :
roles.includes('sponsor') ? 'magenta' :
roles.includes('billing') ? 'cyan' : 'default'
export const rolesToColor = (roles: string[]) => roles.includes('registrant')
? 'green'
: roles.includes('registrar')
? 'purple'
: roles.includes('administrative')
? 'blue'
: roles.includes('technical')
? 'orange'
: roles.includes('sponsor')
? 'magenta'
: roles.includes('billing') ? 'cyan' : 'default'

View File

@@ -1,9 +1,8 @@
import {AxiosError, AxiosResponse} from "axios";
import {MessageInstance, MessageType} from "antd/lib/message/interface";
import {t} from "ttag";
import {AxiosError, AxiosResponse} from 'axios'
import {MessageInstance, MessageType} from 'antd/lib/message/interface'
import {t} from 'ttag'
export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined {
const response = e.response as AxiosResponse
const data = response.data

View File

@@ -1,11 +1,14 @@
import {Domain} from "../api";
import {Domain} from '../api'
export const sortDomainEntities = (domain: Domain) => domain.entities
.filter(e => !e.deleted)
.sort((e1, e2) => {
const p = (r: string[]) => r.includes('registrant') ? 5 :
r.includes('administrative') ? 4 :
r.includes('billing') ? 3 :
r.includes('registrar') ? 2 : 1
const p = (r: string[]) => r.includes('registrant')
? 5
: r.includes('administrative')
? 4
: r.includes('billing')
? 3
: r.includes('registrar') ? 2 : 1
return p(e2.roles) - p(e1.roles)
})

View File

@@ -1,4 +1,4 @@
import {getCountryCode} from "./getCountryCode";
import {getCountryCode} from './getCountryCode'
export const tldToEmoji = (tld: string) => {
if (tld.startsWith('xn--')) return '-'

View File

@@ -1,31 +1,40 @@
import {ConnectorProvider} from "../api/connectors";
import {Typography} from "antd";
import {t} from "ttag";
import React from "react";
import {ConnectorProvider} from '../api/connectors'
import {Typography} from 'antd'
import {t} from 'ttag'
import React from 'react'
export const helpGetTokenLink = (provider?: string) => {
switch (provider) {
case ConnectorProvider.OVH:
return <Typography.Link target='_blank'
href="https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions">
return (
<Typography.Link
target='_blank'
href='https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions'
>
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
</Typography.Link>
)
case ConnectorProvider.GANDI:
return <Typography.Link target='_blank' href="https://admin.gandi.net/organizations/account/pat">
return (
<Typography.Link target='_blank' href='https://admin.gandi.net/organizations/account/pat'>
{t`Retrieve a Personal Access Token from your customer account on the Provider's website`}
</Typography.Link>
)
case ConnectorProvider.NAMECHEAP:
return <Typography.Link target='_blank' href="https://ap.www.namecheap.com/settings/tools/apiaccess/">
return (
<Typography.Link target='_blank' href='https://ap.www.namecheap.com/settings/tools/apiaccess/'>
{t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`}
</Typography.Link>
)
case ConnectorProvider.AUTODNS:
return <Typography.Link target='_blank' href="https://en.autodns.com/domain-robot-api/">
return (
<Typography.Link target='_blank' href='https://en.autodns.com/domain-robot-api/'>
{t`Because of some limitations in API of AutoDNS, we suggest to create an dedicated user for API with limited rights`}
</Typography.Link>
)
default:
return <></>
}
}

View File

@@ -1,5 +1,5 @@
import {t} from "ttag";
import {regionNames} from "../../i18n";
import {t} from 'ttag'
import {regionNames} from '../../i18n'
export const ovhFields = () => ({
appKey: t`Application key`,

29
eslint.config.mjs Normal file
View File

@@ -0,0 +1,29 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.{ts,tsx}"],
languageOptions: {globals: globals.browser},
rules: {
semi: ["error", "never"]
},
},
{
ignores: ["public", "vendor", "webpack.config.js"]
},
{
settings: {
react: {
version: "detect"
}
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];

View File

@@ -18,6 +18,7 @@
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.24.7",
"@eslint/js": "^9.17.0",
"@fontsource/noto-color-emoji": "^5.0.27",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/turbo": "^7.1.1 || ^8.0",
@@ -37,6 +38,9 @@
"axios": "^1.7.2",
"core-js": "^3.23.0",
"dagre": "^0.8.5",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.3",
"globals": "^15.14.0",
"html-loader": "^5.1.0",
"html-to-image": "^1.11.11",
"jsonld": "^8.3.2",
@@ -51,6 +55,7 @@
"ttag": "^1.8.7",
"ttag-cli": "^1.10.12",
"typescript": "^5.5.3",
"typescript-eslint": "^8.19.0",
"vcf": "^2.1.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",

1480
yarn.lock

File diff suppressed because it is too large Load Diff