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 extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
- name: Install dependencies - 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 - name: Run PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Run PHPStan - name: Run PHPStan
run: vendor/bin/phpstan analyse 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 {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, Typography} from 'antd'
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from "react-router-dom"; import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
import TextPage from "./pages/TextPage"; import TextPage from './pages/TextPage'
import DomainSearchPage from "./pages/search/DomainSearchPage"; import DomainSearchPage from './pages/search/DomainSearchPage'
import EntitySearchPage from "./pages/search/EntitySearchPage"; import EntitySearchPage from './pages/search/EntitySearchPage'
import NameserverSearchPage from "./pages/search/NameserverSearchPage"; import NameserverSearchPage from './pages/search/NameserverSearchPage'
import TldPage from "./pages/search/TldPage"; import TldPage from './pages/search/TldPage'
import StatisticsPage from "./pages/StatisticsPage"; import StatisticsPage from './pages/StatisticsPage'
import WatchlistPage from "./pages/tracking/WatchlistPage"; import WatchlistPage from './pages/tracking/WatchlistPage'
import UserPage from "./pages/UserPage"; import UserPage from './pages/UserPage'
import React, {useCallback, useEffect, useMemo, useState} from "react"; import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {getUser} from "./utils/api"; import {getUser} from './utils/api'
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage"; import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
import ConnectorPage from "./pages/tracking/ConnectorPage"; import ConnectorPage from './pages/tracking/ConnectorPage'
import NotFoundPage from "./pages/NotFoundPage"; import NotFoundPage from './pages/NotFoundPage'
import useBreakpoint from "./hooks/useBreakpoint"; import useBreakpoint from './hooks/useBreakpoint'
import {Sider} from "./components/Sider"; import {Sider} from './components/Sider'
import {jt, t} from "ttag"; import {jt, t} from 'ttag'
import {BugOutlined, InfoCircleOutlined, MergeOutlined} from '@ant-design/icons' 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 PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt' 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 ProjectLink = <Typography.Link target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
const LicenseLink = <Typography.Link target='_blank' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link> const 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 navigate = useNavigate()
const location = useLocation() const location = useLocation()
const sm = useBreakpoint('sm') const sm = useBreakpoint('sm')
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)
const authenticated = useCallback((authenticated: boolean) => { const authenticated = useCallback((authenticated: boolean) => {
setIsAuthenticated(authenticated) setIsAuthenticated(authenticated)
}, []); }, [])
const contextValue = useMemo(() => ({ const contextValue = useMemo(() => ({
authenticated, authenticated,
setIsAuthenticated setIsAuthenticated
}), [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) => { const darkModeChange = useCallback((event: MediaQueryListEvent) => {
setDarkMode(event.matches) setDarkMode(event.matches)
}, []) }, [])
useEffect(() => { useEffect(() => {
windowQuery.addEventListener("change", darkModeChange) windowQuery.addEventListener('change', darkModeChange)
return () => { return () => {
windowQuery.removeEventListener("change", darkModeChange) windowQuery.removeEventListener('change', darkModeChange)
} }
}, [windowQuery, darkModeChange]) }, [windowQuery, darkModeChange])
@@ -69,83 +67,91 @@ export default function App() {
}) })
}, []) }, [])
return (
<ConfigProvider
theme={{
algorithm: darkMode ? theme.darkAlgorithm : theme.compactAlgorithm
}}
>
<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} : {})}>
<Sider isAuthenticated={isAuthenticated}/>
</Layout.Sider>
<Layout>
<Layout.Header style={{padding: 0}}/>
<Layout.Content style={sm ? {margin: '24px 0'} : {margin: '24px 16px 0'}}>
<div style={{
padding: 24,
minHeight: 360
}}
>
<Routes>
<Route path='/' element={<Navigate to='/login'/>}/>
<Route path='/home' element={<TextPage resource='home.md'/>}/>
return <ConfigProvider <Route path='/search/domain' element={<DomainSearchPage/>}/>
theme={{ <Route path='/search/domain/:query' element={<DomainSearchPage/>}/>
algorithm: darkMode ? theme.darkAlgorithm : theme.compactAlgorithm <Route path='/search/entity' element={<EntitySearchPage/>}/>
}} <Route path='/search/nameserver' element={<NameserverSearchPage/>}/>
> <Route path='/search/tld' element={<TldPage/>}/>
<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} : {})}>
<Sider isAuthenticated={isAuthenticated}/>
</Layout.Sider>
<Layout>
<Layout.Header style={{padding: 0}}/>
<Layout.Content style={sm ? {margin: '24px 0'} : {margin: '24px 16px 0'}}>
<div style={{
padding: 24,
minHeight: 360,
}}>
<Routes>
<Route path="/" element={<Navigate to="/login"/>}/>
<Route path="/home" element={<TextPage resource='home.md'/>}/>
<Route path="/search/domain" element={<DomainSearchPage/>}/> <Route path='/tracking/watchlist' element={<WatchlistPage/>}/>
<Route path="/search/domain/:query" element={<DomainSearchPage/>}/> <Route path='/tracking/domains' element={<TrackedDomainPage/>}/>
<Route path="/search/entity" element={<EntitySearchPage/>}/> <Route path='/tracking/connectors' element={<ConnectorPage/>}/>
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
<Route path="/search/tld" element={<TldPage/>}/>
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/> <Route path='/stats' element={<StatisticsPage/>}/>
<Route path="/tracking/domains" element={<TrackedDomainPage/>}/> <Route path='/user' element={<UserPage/>}/>
<Route path="/tracking/connectors" element={<ConnectorPage/>}/>
<Route path="/stats" element={<StatisticsPage/>}/> <Route path='/faq' element={<TextPage resource='faq.md'/>}/>
<Route path="/user" element={<UserPage/>}/> <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='/login' element={<LoginPage/>}/>
<Route path="/tos" element={<TextPage resource='tos.md'/>}/>
<Route path="/privacy" element={<TextPage resource='privacy.md'/>}/>
<Route path="/login" element={<LoginPage/>}/> <Route path='*' element={<NotFoundPage/>}/>
</Routes>
<Route path="*" element={<NotFoundPage/>}/> </div>
</Routes> </Layout.Content>
</div> <Layout.Footer style={{textAlign: 'center'}}>
</Layout.Content> <Space size='middle' wrap align='center'>
<Layout.Footer style={{textAlign: 'center'}}> <Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
<Space size='middle' wrap align='center'> <Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link> <Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link> <Typography.Link
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link> target='_blank'
<Typography.Link target='_blank' href='https://github.com/maelgangloff/domain-watchdog/wiki'
href='https://github.com/maelgangloff/domain-watchdog/wiki'><Button >
type='text'>{t`Documentation`}</Button></Typography.Link> <Button
</Space> type='text'
<Typography.Paragraph style={{marginTop: '1em'}}> >{t`Documentation`}
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`} </Button>
</Typography.Paragraph> </Typography.Link>
</Layout.Footer> </Space>
<Typography.Paragraph style={{marginTop: '1em'}}>
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
</Typography.Paragraph>
</Layout.Footer>
</Layout>
<FloatButton.Group
trigger='hover'
style={{
position: 'fixed',
insetInlineEnd: (100 - 40) / 2,
bottom: 100 - 40 / 2
}}
icon={<InfoCircleOutlined/>}
>
<Tooltip title={t`Official git repository`} placement='left'>
<FloatButton icon={<MergeOutlined/>} target='_blank' href={PROJECT_LINK}/>
</Tooltip>
<Tooltip title={t`Submit an issue`} placement='left'>
<FloatButton icon={<BugOutlined/>} target='_blank' href={PROJECT_LINK + '/issues'}/>
</Tooltip>
</FloatButton.Group>
</Layout> </Layout>
<FloatButton.Group </AuthenticatedContext.Provider>
trigger='hover' </ConfigProvider>
style={{ )
position: 'fixed', }
insetInlineEnd: (100 - 40) / 2,
bottom: 100 - 40 / 2,
}}
icon={<InfoCircleOutlined/>}
>
<Tooltip title={t`Official git repository`} placement='left'>
<FloatButton icon={<MergeOutlined/>} target='_blank' href={PROJECT_LINK}/>
</Tooltip>
<Tooltip title={t`Submit an issue`} placement='left'>
<FloatButton icon={<BugOutlined/>} target='_blank' href={PROJECT_LINK + '/issues'}/>
</Tooltip>
</FloatButton.Group>
</Layout>
</AuthenticatedContext.Provider>
</ConfigProvider>
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,19 @@
import {StepProps, Steps, Tooltip} from "antd"; import {StepProps, Steps, Tooltip} from 'antd'
import React from "react"; import React from 'react'
import {t} from "ttag"; import {t} from 'ttag'
import { import {
CheckOutlined, CheckOutlined,
DeleteOutlined, DeleteOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
FieldTimeOutlined, FieldTimeOutlined,
SignatureOutlined SignatureOutlined
} from "@ant-design/icons"; } from '@ant-design/icons'
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation"; import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from '../../utils/functions/rdapTranslation'
export function DomainLifecycleSteps({status}: { status: string[] }) { export function DomainLifecycleSteps({status}: { status: string[] }) {
const rdapEventDetailTranslated = rdapEventDetailTranslation() const rdapEventDetailTranslated = rdapEventDetailTranslation()
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation() const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
const steps: StepProps[] = [ const steps: StepProps[] = [
{ {
title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>, title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>,
@@ -26,16 +24,19 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
icon: <CheckOutlined/> 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'}}/> icon: <FieldTimeOutlined style={{color: 'palevioletred'}}/>
}, },
{ {
title: <Tooltip 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'}}/> 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'}}/> icon: <DeleteOutlined style={{color: 'orangered'}}/>
} }
] ]
@@ -50,8 +51,10 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
currentStep = 4 currentStep = 4
} }
return <Steps return (
current={currentStep} <Steps
items={steps} current={currentStep}
/> items={steps}
} />
)
}

View File

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

View File

@@ -1,38 +1,44 @@
import {Form, Input} from "antd"; import {Form, Input} from 'antd'
import {t} from "ttag"; import {t} from 'ttag'
import {SearchOutlined} from "@ant-design/icons"; import {SearchOutlined} from '@ant-design/icons'
import React from "react"; import React from 'react'
export type FieldType = { export interface FieldType {
ldhName: string ldhName: string
} }
export function DomainSearchBar({onFinish, initialValue}: { onFinish: (values: FieldType) => void, initialValue?: string }) { export function DomainSearchBar({onFinish, initialValue}: {
return <Form onFinish: (values: FieldType) => void,
onFinish={onFinish} initialValue?: string
autoComplete="off" }) {
style={{width: '100%'}} return (
> <Form
<Form.Item<FieldType> onFinish={onFinish}
name="ldhName" autoComplete='off'
initialValue={initialValue} style={{width: '100%'}}
rules={[{
required: true,
message: t`Required`
}, {
pattern: /^(?=.*\.)?\S*[^.\s]$/,
message: t`This domain name does not appear to be valid`,
max: 63,
min: 2
}]}
> >
<Input style={{textAlign: 'center'}} <Form.Item<FieldType>
size="large" name='ldhName'
prefix={<SearchOutlined/>} initialValue={initialValue}
placeholder="example.com" rules={[{
autoComplete='off' required: true,
autoFocus message: t`Required`
/> }, {
</Form.Item> pattern: /^(?=.*\.)?\S*[^.\s]$/,
</Form> message: t`This domain name does not appear to be valid`,
} max: 63,
min: 2
}]}
>
<Input
style={{textAlign: 'center'}}
size='large'
prefix={<SearchOutlined/>}
placeholder='example.com'
autoComplete='off'
autoFocus
/>
</Form.Item>
</Form>
)
}

View File

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

View File

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

View File

@@ -1,21 +1,29 @@
import {Tag} from "antd"; import {Tag} from 'antd'
import {DeleteOutlined, ExclamationCircleOutlined} from "@ant-design/icons"; import {DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
import punycode from "punycode/punycode"; import punycode from 'punycode/punycode'
import {Link} from "react-router-dom"; import {Link} from 'react-router-dom'
import React from "react"; import React from 'react'
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[] } }) { export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[] } }) {
return <Link to={'/search/domain/' + domain.ldhName}> return (
<Tag <Link to={'/search/domain/' + domain.ldhName}>
color={ <Tag
domain.deleted ? 'magenta' : color={
domain.status.includes('redemption period') ? 'yellow' : domain.deleted
domain.status.includes('pending delete') ? 'volcano' : 'default' ? 'magenta'
} : domain.status.includes('redemption period')
icon={ ? 'yellow'
domain.deleted ? <DeleteOutlined/> : : domain.status.includes('pending delete') ? 'volcano' : 'default'
domain.status.includes('redemption period') ? <ExclamationCircleOutlined/> : }
domain.status.includes('pending delete') ? <DeleteOutlined/> : null icon={
}>{punycode.toUnicode(domain.ldhName)}</Tag> domain.deleted
</Link> ? <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 {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from 'antd'
import React, {useState} from "react"; import React, {useState} from 'react'
import {Connector, ConnectorProvider} from "../../../utils/api/connectors"; import {Connector, ConnectorProvider} from '../../../utils/api/connectors'
import {t} from "ttag"; import {t} from 'ttag'
import {BankOutlined} from "@ant-design/icons"; import {BankOutlined} from '@ant-design/icons'
import { import {
ovhEndpointList as ovhEndpointListFunction, ovhEndpointList as ovhEndpointListFunction,
ovhFields as ovhFieldsFunction, ovhFields as ovhFieldsFunction,
ovhPricingMode as ovhPricingModeFunction, ovhPricingMode as ovhPricingModeFunction,
ovhSubsidiaryList as ovhSubsidiaryListFunction ovhSubsidiaryList as ovhSubsidiaryListFunction
} from "../../../utils/providers/ovh"; } from '../../../utils/providers/ovh'
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers"; import {helpGetTokenLink, tosHyperlink} from '../../../utils/providers'
const formItemLayoutWithOutLabel = { const formItemLayoutWithOutLabel = {
wrapperCol: { wrapperCol: {
xs: {span: 24, offset: 0}, 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 }) { export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
@@ -27,229 +27,257 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [ovhPricingModeValue, setOvhPricingModeValue] = useState<string | undefined>() const [ovhPricingModeValue, setOvhPricingModeValue] = useState<string | undefined>()
return (
return <Form <Form
{...formItemLayoutWithOutLabel} {...formItemLayoutWithOutLabel}
form={form} form={form}
layout="horizontal" layout='horizontal'
labelCol={{span: 6}} labelCol={{span: 6}}
wrapperCol={{span: 14}} wrapperCol={{span: 14}}
onFinish={onCreate} onFinish={onCreate}
>
<Form.Item
label={t`Provider`}
name="provider"
help={helpGetTokenLink(provider)}
rules={[{required: true, message: t`Required`}]}
> >
<Select <Form.Item
allowClear label={t`Provider`}
placeholder={t`Please select a Provider`} name='provider'
suffixIcon={<BankOutlined/>} help={helpGetTokenLink(provider)}
options={Object.keys(ConnectorProvider).map((c) => ({ rules={[{required: true, message: t`Required`}]}
value: ConnectorProvider[c as keyof typeof ConnectorProvider], >
label: ( <Select
<> allowClear
<BankOutlined/>{" "} {c} placeholder={t`Please select a Provider`}
</> suffixIcon={<BankOutlined/>}
), options={Object.keys(ConnectorProvider).map((c) => ({
}))} value: ConnectorProvider[c as keyof typeof ConnectorProvider],
value={provider} label: (
onChange={setProvider} <>
autoFocus <BankOutlined/>{' '} {c}
/> </>
</Form.Item> )
}))}
value={provider}
onChange={setProvider}
autoFocus
/>
</Form.Item>
{ {
provider === ConnectorProvider.OVH && <> provider === ConnectorProvider.OVH && <>
{ {
Object.keys(ovhFields).map(fieldName => <Form.Item Object.keys(ovhFields).map(fieldName => <Form.Item
label={ovhFields[fieldName as keyof typeof ovhFields]} key={ovhFields[fieldName as keyof typeof ovhFields]}
name={['authData', fieldName]} label={ovhFields[fieldName as keyof typeof ovhFields]}
name={['authData', fieldName]}
rules={[{required: true, message: t`Required`}]}
>
<Input autoComplete='off'/>
</Form.Item>)
}
<Form.Item
label={t`OVH Endpoint`}
name={['authData', 'apiEndpoint']}
rules={[{required: true, message: t`Required`}]} rules={[{required: true, message: t`Required`}]}
> >
<Input autoComplete='off'/> <Select options={ovhEndpointList} optionFilterProp='label'/>
</Form.Item>) </Form.Item>
} <Form.Item
<Form.Item label={t`OVH subsidiary`}
label={t`OVH Endpoint`} name={['authData', 'ovhSubsidiary']}
name={['authData', 'apiEndpoint']} rules={[{required: true, message: t`Required`}]}
rules={[{required: true, message: t`Required`}]}
>
<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"/>
</Form.Item>
<Form.Item
label={t`OVH pricing mode`}
name={['authData', 'pricingMode']}
rules={[{required: true, message: t`Required`}]}
>
<Popconfirm
title={t`Confirm pricing mode`}
description={t`Are you sure about this setting? This may result in additional charges from the API Provider`}
onCancel={() => {
form.resetFields(['authData'])
setOvhPricingModeValue(undefined)
setOpen(false)
}}
onConfirm={() => setOpen(false)}
open={open}
> >
<Select options={ovhPricingMode} optionFilterProp="label" value={ovhPricingModeValue} <Select options={ovhSubsidiaryList} optionFilterProp='label'/>
</Form.Item>
<Form.Item
label={t`OVH pricing mode`}
name={['authData', 'pricingMode']}
rules={[{required: true, message: t`Required`}]}
>
<Popconfirm
title={t`Confirm pricing mode`}
description={t`Are you sure about this setting? This may result in additional charges from the API Provider`}
onCancel={() => {
form.resetFields(['authData'])
setOvhPricingModeValue(undefined)
setOpen(false)
}}
onConfirm={() => setOpen(false)}
open={open}
>
<Select
options={ovhPricingMode} optionFilterProp='label' value={ovhPricingModeValue}
onChange={(value: string) => { onChange={(value: string) => {
setOvhPricingModeValue(value) setOvhPricingModeValue(value)
form.setFieldValue(['authData', 'pricingMode'], value) form.setFieldValue(['authData', 'pricingMode'], value)
if (value !== 'create-default') { if (value !== 'create-default') {
setOpen(true) setOpen(true)
} }
}}/> }}
</Popconfirm> />
</Form.Item> </Popconfirm>
</> </Form.Item>
} </>
{ }
provider === ConnectorProvider.GANDI && <> {
<Form.Item provider === ConnectorProvider.GANDI && <>
label={t`Personal Access Token (PAT)`} <Form.Item
name={['authData', 'token']} label={t`Personal Access Token (PAT)`}
rules={[{required: true, message: t`Required`}]}> name={['authData', 'token']}
<Input autoComplete='off'/> rules={[{required: true, message: t`Required`}]}
</Form.Item> >
<Form.Item <Input autoComplete='off'/>
label={t`Organization sharing ID`} </Form.Item>
name={['authData', 'sharingId']} <Form.Item
help={<Typography.Text label={t`Organization sharing ID`}
type='secondary'>{t`It indicates the organization that will pay for the ordered product`}</Typography.Text>} name={['authData', 'sharingId']}
required={false}> help={<Typography.Text
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/> type='secondary'
</Form.Item> >{t`It indicates the organization that will pay for the ordered product`}
</> </Typography.Text>}
} required={false}
{ >
provider === ConnectorProvider.AUTODNS && <> <Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
<Alert </Form.Item>
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"/> }
<br/> {
<Form.Item provider === ConnectorProvider.AUTODNS && <>
label={t`AutoDNS Username`} <Alert
name={['authData', 'username']} message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
help={<Typography.Text type='warning'
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`}]}> <br/>
<Input autoComplete='off' required={true}/> <Form.Item
</Form.Item> label={t`AutoDNS Username`}
<Form.Item name={['authData', 'username']}
label={t`AutoDNS Password`} help={<Typography.Text
name={['authData', 'password']} type='secondary'
rules={[{required: true, message: t`Required`}]} >{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}
required={true}> </Typography.Text>}
<Input.Password autoComplete='off' required={true} placeholder=''/> rules={[{required: true, message: t`Required`}]}
</Form.Item> >
<Form.Item <Input autoComplete='off' required/>
label={t`Owner nic-handle`} </Form.Item>
name={['authData', 'contactid']} <Form.Item
help={<Typography.Text label={t`AutoDNS Password`}
type='secondary'>{t`The nic-handle of the domain name owner`}<a name={['authData', 'password']}
href="https://cloud.autodns.com/contacts/domain">{t`You can get it from this page`}</a></Typography.Text>} rules={[{required: true, message: t`Required`}]}
rules={[{required: true, message: t`Required`}]} required
required={true}> >
<Input autoComplete='off' required={true} placeholder=''/> <Input.Password autoComplete='off' required placeholder=''/>
</Form.Item> </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>}
rules={[{required: true, message: t`Required`}]}
required
>
<Input autoComplete='off' required placeholder=''/>
</Form.Item>
<Form.Item <Form.Item
label={t`Context Value`} label={t`Context Value`}
name={['authData', 'context']} name={['authData', 'context']}
help={<Typography.Text 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> <Input autoComplete='off' required={false} placeholder='4'/>
</Form.Item>
<Form.Item <Form.Item
valuePropName='checked' valuePropName='checked'
label={t`Owner confirmation`} label={t`Owner confirmation`}
name={['authData', 'ownerConfirm']} name={['authData', 'ownerConfirm']}
rules={[{required: true, message: t`Required`}]} rules={[{required: true, message: t`Required`}]}
> >
<Checkbox <Checkbox
required={true}>{t`Owner confirms his consent of domain order jobs`}</Checkbox> required
</Form.Item> >{t`Owner confirms his consent of domain order jobs`}
</> </Checkbox>
</Form.Item>
</>
} }
{ {
provider === ConnectorProvider.NAMECHEAP && <> provider === ConnectorProvider.NAMECHEAP && <>
<Form.Item <Form.Item
label={t`Username`} label={t`Username`}
name={['authData', 'ApiUser']} name={['authData', 'ApiUser']}
> >
<Input autoComplete='off'></Input> <Input autoComplete='off'/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t`API key`} label={t`API key`}
name={['authData', 'ApiKey']} name={['authData', 'ApiKey']}
> >
<Input autoComplete='off'></Input> <Input autoComplete='off'/>
</Form.Item> </Form.Item>
</> </>
} }
{ {
provider !== undefined && <> provider !== undefined && <>
<Form.Item <Form.Item
valuePropName="checked" valuePropName='checked'
label={t`API Terms of Service`} label={t`API Terms of Service`}
name={['authData', 'acceptConditions']} name={['authData', 'acceptConditions']}
rules={[{required: true, message: t`Required`}]} rules={[{required: true, message: t`Required`}]}
style={{marginTop: '3em'}} style={{marginTop: '3em'}}
> >
<Checkbox <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 target='_blank' href={tosHyperlink(provider)}>
</Typography.Link> {t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
</Checkbox> </Typography.Link>
</Form.Item> </Checkbox>
<Form.Item </Form.Item>
valuePropName="checked" <Form.Item
label={t`Legal age`} valuePropName='checked'
name={['authData', 'ownerLegalAge']} label={t`Legal age`}
rules={[{required: true, message: t`Required`}]} 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> <Checkbox
</Form.Item> required
<Form.Item >{t`I am of the minimum age required to consent to these conditions`}
valuePropName="checked" </Checkbox>
label={t`Withdrawal period`} </Form.Item>
name={['authData', 'waiveRetractationPeriod']} <Form.Item
rules={[{required: true, message: t`Required`}]} valuePropName='checked'
> label={t`Withdrawal period`}
<Checkbox name={['authData', 'waiveRetractationPeriod']}
required={true}>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}</Checkbox> rules={[{required: true, message: t`Required`}]}
</Form.Item> >
</> <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'}}> <Form.Item style={{marginTop: '5vh'}}>
<Space> <Space>
<Button type="primary" htmlType="submit"> <Button type='primary' htmlType='submit'>
{t`Create`} {t`Create`}
</Button> </Button>
<Button type="default" htmlType="reset"> <Button type='default' htmlType='reset'>
{t`Reset`} {t`Reset`}
</Button> </Button>
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>
} )
}

View File

@@ -1,11 +1,10 @@
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd"; import {Card, Divider, message, Popconfirm, theme, Typography} from 'antd'
import {t} from "ttag"; import {t} from 'ttag'
import {DeleteFilled} from "@ant-design/icons"; import {DeleteFilled} from '@ant-design/icons'
import React from "react"; import React from 'react'
import {Connector, deleteConnector} from "../../../utils/api/connectors"; import {Connector, deleteConnector} from '../../../utils/api/connectors'
const {useToken} = theme;
const {useToken} = theme
export type ConnectorElement = Connector & { id: string, createdAt: string } export type ConnectorElement = Connector & { id: string, createdAt: string }
@@ -13,28 +12,36 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
const {token} = useToken() const {token} = useToken()
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id) const onConnectorDelete = async (connector: ConnectorElement) => await deleteConnector(connector.id)
.then(onDelete) .then(onDelete)
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`)) .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 => <>
<> {connectors.map(connector =>
{contextHolder} <>
<Card hoverable title={<Typography.Text {contextHolder}
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>} <Card
size='small' hoverable title={<Typography.Text
style={{width: '100%'}} title={new Date(connector.createdAt).toLocaleString()}
extra={<Popconfirm title={t`Delete the Connector`} >{t`Connector ${connector.provider}`}
description={t`Are you sure to delete this Connector?`} </Typography.Text>}
onConfirm={() => onConnectorDelete(connector)} size='small'
okText={t`Yes`} style={{width: '100%'}}
cancelText={t`No`} extra={<Popconfirm
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}> title={t`Delete the Connector`}
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/> description={t`Are you sure to delete this Connector?`}
</Card> onConfirm={async () => await onConnectorDelete(connector)}
<Divider/> okText={t`Yes`}
</> cancelText={t`No`}
)} ><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 {CalendarFilled} from '@ant-design/icons'
import {t} from "ttag"; import {t} from 'ttag'
import {Popover, QRCode, Typography} from "antd"; import {Popover, QRCode, Typography} from 'antd'
import React from "react"; import React from 'react'
import {Watchlist} from "../../../pages/tracking/WatchlistPage"; import {Watchlist} from '../../../utils/api'
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) { export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar` const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
return <Typography.Link href={icsResourceLink}> return (
<Popover content={<QRCode value={icsResourceLink} <Typography.Link href={icsResourceLink}>
bordered={false} <Popover content={<QRCode
title={t`QR Code for iCalendar export`} value={icsResourceLink}
type='svg' bordered={false}
/>}> title={t`QR Code for iCalendar export`}
<CalendarFilled title={t`Export events to iCalendar format`} type='svg'
style={{color: 'limegreen'}} />}
/> >
</Popover> <CalendarFilled
</Typography.Link> 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 {Popconfirm, theme, Typography} from 'antd'
import {t} from "ttag"; import {t} from 'ttag'
import {deleteWatchlist} from "../../../utils/api"; import {deleteWatchlist, Watchlist} from '../../../utils/api'
import {DeleteFilled} from "@ant-design/icons"; import {DeleteFilled} from '@ant-design/icons'
import React from "react"; import React from 'react'
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) { export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
const {token} = theme.useToken() const {token} = theme.useToken()
return <Popconfirm return (
title={t`Delete the Watchlist`} <Popconfirm
description={t`Are you sure to delete this Watchlist?`} title={t`Delete the Watchlist`}
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)} description={t`Are you sure to delete this Watchlist?`}
okText={t`Yes`} onConfirm={async () => await deleteWatchlist(watchlist.token).then(onDelete)}
cancelText={t`No`} okText={t`Yes`}
okButtonProps={{danger: true}}> cancelText={t`No`}
<Typography.Link> okButtonProps={{danger: true}}
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/> >
</Typography.Link> <Typography.Link>
</Popconfirm> <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 React, {ReactElement, useEffect, useState} from 'react'
import {Domain, getTrackedDomainList} from "../../../utils/api"; import {Domain, getTrackedDomainList} from '../../../utils/api'
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from "antd"; import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from 'antd'
import {t} from "ttag"; import {t} from 'ttag'
import {ColumnType} from "antd/es/table"; import {ColumnType} from 'antd/es/table'
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation"; import {rdapStatusCodeDetailTranslation} from '../../../utils/functions/rdapTranslation'
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor"; import {eppStatusCodeToColor} from '../../../utils/functions/eppStatusCodeToColor'
import {Link} from "react-router-dom"; import {Link} from 'react-router-dom'
import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons' import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons'
import {DomainToTag} from "../DomainToTag"; import {DomainToTag} from '../DomainToTag'
export function TrackedDomainTable() { export function TrackedDomainTable() {
const REDEMPTION_NOTICE = <Tooltip const REDEMPTION_NOTICE = (
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}> <Tooltip
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag> title={t`At least one domain name is in redemption period and will potentially be deleted soon`}
</Tooltip> >
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
</Tooltip>
)
const PENDING_DELETE_NOTICE = <Tooltip const PENDING_DELETE_NOTICE = (
title={t`At least one domain name is pending deletion and will soon become available for registration again`}> <Tooltip
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag> title={t`At least one domain name is pending deletion and will soon become available for registration again`}
</Tooltip> >
<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 [total, setTotal] = useState<number>()
const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([]) const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([])
@@ -46,8 +60,10 @@ export function TrackedDomainTable() {
ldhName: <DomainToTag domain={d}/>, ldhName: <DomainToTag domain={d}/>,
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-', expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
status: d.status.map(s => <Tooltip status: d.status.map(s => <Tooltip
key={s}
placement='bottomLeft' placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}> title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag> <Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip> </Tooltip>
), ),
@@ -63,17 +79,19 @@ export function TrackedDomainTable() {
fetchData({page: 1, itemsPerPage: 30}) fetchData({page: 1, itemsPerPage: 30})
}, []) }, [])
interface RecordType {
domain: Domain
}
const columns: ColumnType<any>[] = [ const columns: Array<ColumnType<RecordType>> = [
{ {
title: t`Domain`, title: t`Domain`,
dataIndex: "ldhName" dataIndex: 'ldhName'
}, },
{ {
title: t`Expiration date`, title: t`Expiration date`,
dataIndex: 'expirationDate', 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 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 const expirationDate2 = b.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
@@ -85,65 +103,70 @@ export function TrackedDomainTable() {
{ {
title: t`Updated at`, title: t`Updated at`,
dataIndex: 'updatedAt', dataIndex: 'updatedAt',
sorter: (a: { domain: Domain }, b: { sorter: (a: RecordType, b: RecordType) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
domain: Domain
}) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
}, },
{ {
title: t`Status`, title: t`Status`,
dataIndex: 'status', dataIndex: 'status',
showSorterTooltip: {target: 'full-header'}, 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 text: <Tooltip
placement='bottomLeft' placement='bottomLeft'
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}> title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
>
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag> <Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
</Tooltip>, </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 {
description={t`No tracked domain names were found, please create your first Watchlist`} total === 0
> ? <Empty
<Link to='/tracking/watchlist'> description={t`No tracked domain names were found, please create your first Watchlist`}
<Button type="primary">Create Now</Button> >
</Link> <Link to='/tracking/watchlist'>
</Empty> : <Skeleton loading={total === undefined}> <Button type='primary'>Create Now</Button>
<Result </Link>
style={{paddingTop: 0}} </Empty>
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`} : <Skeleton loading={total === undefined}>
{...(specialNotice.length > 0 ? { <Result
icon: <ExceptionOutlined/>, style={{paddingTop: 0}}
status: 'warning', subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
title: t`At least one domain name you are tracking requires special attention`, {...(specialNotice.length > 0
extra: specialNotice ? {
} : { icon: <ExceptionOutlined/>,
icon: <MonitorOutlined/>, status: 'warning',
status: 'info', title: t`At least one domain name you are tracking requires special attention`,
title: t`The domain names below are subject to special monitoring`, extra: specialNotice
})} }
/> : {
icon: <MonitorOutlined/>,
status: 'info',
title: t`The domain names below are subject to special monitoring`
})}
/>
<Table <Table
loading={total === undefined} loading={total === undefined}
columns={columns} columns={columns}
dataSource={dataTable} dataSource={dataTable}
pagination={{ pagination={{
total, total,
hideOnSinglePage: true, hideOnSinglePage: true,
defaultPageSize: 30, defaultPageSize: 30,
onChange: (page, itemsPerPage) => { onChange: (page, itemsPerPage) => {
fetchData({page, itemsPerPage}) fetchData({page, itemsPerPage})
} }
}} }}
scroll={{y: '50vh'}} scroll={{y: '50vh'}}
/> />
</Skeleton> </Skeleton>
} }
</> </>
} )
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,20 @@
import {Button, Flex, Modal, Space, Typography} from "antd" import {Button, Flex, Modal, Space, Typography} from 'antd'
import {t} from "ttag" import {t} from 'ttag'
import React, {useEffect, useState} from "react" import React, {useEffect, useState} from 'react'
import {ApartmentOutlined} from "@ant-design/icons" import {ApartmentOutlined} from '@ant-design/icons'
import '@xyflow/react/dist/style.css' import '@xyflow/react/dist/style.css'
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react"; import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
import {getWatchlist} from "../../../../utils/api"; import {getWatchlist} from '../../../../utils/api'
import {getLayoutedElements} from "./getLayoutedElements"; import {getLayoutedElements} from './getLayoutedElements'
import {watchlistToNodes} from "./watchlistToNodes"; import {watchlistToNodes} from './watchlistToNodes'
import {watchlistToEdges} from "./watchlistToEdges"; import {watchlistToEdges} from './watchlistToEdges'
export function ViewDiagramWatchlistButton({token}: { token: string }) { export function ViewDiagramWatchlistButton({token}: { token: string }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [nodes, setNodes, onNodesChange] = useNodesState([]) const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
useEffect(() => { useEffect(() => {
setEdges([]) setEdges([])
@@ -30,52 +29,54 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
setNodes(e.nodes) setNodes(e.nodes)
setEdges(e.edges) setEdges(e.edges)
}).catch(() => setOpen(false)).finally(() => setLoading(false)) }).catch(() => setOpen(false)).finally(() => setLoading(false))
}, [open]) }, [open])
return (
return <> <>
<Typography.Link> <Typography.Link>
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`} <ApartmentOutlined
style={{color: 'darkviolet'}} title={t`View the Watchlist Entity Diagram`}
onClick={() => setOpen(true)}/> style={{color: 'darkviolet'}}
</Typography.Link> onClick={() => setOpen(true)}
<Modal />
title={t`Watchlist Entity Diagram`} </Typography.Link>
centered <Modal
open={open} title={t`Watchlist Entity Diagram`}
loading={loading} centered
footer={ open={open}
<Space> loading={loading}
<Button type="default" onClick={() => setOpen(false)}> footer={
Close <Space>
</Button> <Button type='default' onClick={() => setOpen(false)}>
</Space> Close
} </Button>
onOk={() => setOpen(false)} </Space>
onCancel={() => setOpen(false)} }
width='90vw' onOk={() => setOpen(false)}
height='100%' onCancel={() => setOpen(false)}
> width='90vw'
<Flex style={{width: '85vw', height: '85vh'}}> height='100%'
<ReactFlow >
fitView <Flex style={{width: '85vw', height: '85vh'}}>
colorMode='dark' <ReactFlow
defaultEdges={[]} fitView
defaultNodes={[]} colorMode='dark'
nodesConnectable={false} defaultEdges={[]}
edgesReconnectable={false} defaultNodes={[]}
nodes={nodes} nodesConnectable={false}
edges={edges} edgesReconnectable={false}
onNodesChange={onNodesChange} nodes={nodes}
onEdgesChange={onEdgesChange} edges={edges}
style={{width: '100%', height: '100%'}} onNodesChange={onNodesChange}
> onEdgesChange={onEdgesChange}
<MiniMap/> style={{width: '100%', height: '100%'}}
<Controls/> >
<Background/> <MiniMap/>
</ReactFlow> <Controls/>
</Flex> <Background/>
</Modal> </ReactFlow>
</> </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() const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({})) dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodeWidth = 172 const nodeWidth = 172
const nodeHeight = 200 const nodeHeight = 200
const isHorizontal = direction === 'LR'; const isHorizontal = direction === 'LR'
dagreGraph.setGraph({rankdir: direction}); dagreGraph.setGraph({rankdir: direction})
nodes.forEach((node: any) => { nodes.forEach(node => {
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight}); dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight})
}); })
edges.forEach((edge: any) => { edges.forEach(edge => {
dagreGraph.setEdge(edge.source, edge.target); 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) const nodeWithPosition = dagreGraph.node(node.id)
return { return {
...node, ...node,
targetPosition: isHorizontal ? 'left' : 'top', targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? 'right' : 'bottom', sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: { position: {
x: nodeWithPosition.x - nodeWidth / 2, x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 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 {Domain, Watchlist} from '../../../../utils/api'
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation"; import {rdapRoleTranslation} from '../../../../utils/functions/rdapTranslation'
import {t} from "ttag"; 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 rdapRoleTranslated = rdapRoleTranslation()
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor')) const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
return d.entities return d.entities
.filter(e => .filter(e =>
!e.deleted && !e.deleted &&
(withRegistrar || !e.roles.includes('registrar')) && (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 => ({ .map(e => ({
id: `e-${d.ldhName}-${e.entity.handle}`, id: `e-${d.ldhName}-${e.entity.handle}`,
@@ -21,11 +22,11 @@ export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
label: e.roles label: e.roles
.map(r => rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r) .map(r => rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r)
.join(', '), .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 => ({ .map(ns => ({
id: `ns-${d.ldhName}-${ns.ldhName}`, id: `ns-${d.ldhName}-${ns.ldhName}`,
source: d.ldhName, source: d.ldhName,
@@ -34,7 +35,7 @@ export const domainNSToEdges = (d: Domain) => d.nameservers
label: 'DNS' label: 'DNS'
})) }))
export const tldToEdge = (d: Domain) => ({ export const tldToEdge = (d: Domain): Edge => ({
id: `tld-${d.ldhName}-${d.tld.tld}`, id: `tld-${d.ldhName}-${d.tld.tld}`,
source: d.tld.tld, source: d.tld.tld,
target: d.ldhName, target: d.ldhName,
@@ -42,7 +43,7 @@ export const tldToEdge = (d: Domain) => ({
label: t`Registry` 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 entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat() const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
const tldEdge = watchlist.domains.map(tldToEdge) const tldEdge = watchlist.domains.map(tldToEdge)

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import {Breakpoint, theme} from 'antd'; import {Breakpoint, theme} from 'antd'
import {useMediaQuery} from 'react-responsive'; 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 => { const propertyName = (breakpoint: Breakpoint): ScreenProperty => {
return 'screen' + breakpoint.toUpperCase() as ScreenProperty return 'screen' + breakpoint.toUpperCase() as ScreenProperty
}; }
export default function useBreakpoint( export default function useBreakpoint(
breakpoint: Breakpoint breakpoint: Breakpoint

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import {Result} from "antd"; import {Result} from 'antd'
import React from "react"; import React from 'react'
import {t} from 'ttag' import {t} from 'ttag'
export default function NotFoundPage() { export default function NotFoundPage() {
return <Result return (
status="404" <Result
title="404" status='404'
subTitle={t`Sorry, the page you visited does not exist.`} 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 React, {useEffect, useState} from 'react'
import {getStatistics, Statistics} from "../utils/api"; import {getStatistics, Statistics} from '../utils/api'
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd"; import {Card, Col, Divider, Row, Statistic, Tooltip} from 'antd'
import {t} from "ttag"; import {t} from 'ttag'
import { import {
AimOutlined, AimOutlined,
CompassOutlined, CompassOutlined,
DatabaseOutlined, DatabaseOutlined,
FieldTimeOutlined, FieldTimeOutlined,
NotificationOutlined NotificationOutlined
} from "@ant-design/icons"; } from '@ant-design/icons'
export default function StatisticsPage() { export default function StatisticsPage() {
const [stats, setStats] = useState<Statistics>() const [stats, setStats] = useState<Statistics>()
useEffect(() => { useEffect(() => {
@@ -20,101 +19,104 @@ export default function StatisticsPage() {
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0) const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
const successRate = stats !== undefined ? const successRate = stats !== undefined
(totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase) ? (totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
: undefined : undefined
return <> return (
<Row gutter={16}> <>
<Col span={12}> <Row gutter={16}>
<Card bordered={false}> <Col span={12}>
<Statistic
loading={stats === undefined}
prefix={<CompassOutlined/>}
title={t`RDAP queries`}
value={stats?.rdapQueries}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Alerts sent`}
prefix={<NotificationOutlined/>}
value={stats?.alertSent}
valueStyle={{color: 'violet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain names in database`}
prefix={<DatabaseOutlined/>}
value={stats?.domainCountTotal}
valueStyle={{color: 'orange'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Tracked domain names`}
prefix={<AimOutlined/>}
value={stats?.domainTracked}
valueStyle={{color: 'violet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Purchased domain names`}
prefix={<FieldTimeOutlined/>}
value={stats?.domainPurchased}
valueStyle={{color: 'green'}}
/>
</Card>
</Col>
<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.`}>
<Statistic
loading={stats === undefined}
title={t`Success rate`}
value={successRate === undefined ? '-' : successRate * 100}
suffix='%'
valueStyle={{color: successRate === undefined ? 'grey' : successRate >= 0.5 ? 'darkgreen' : 'orange'}}
/>
</Tooltip>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16} justify='center' align='middle'>
{stats?.domainCount
.sort((a, b) => b.domain - a.domain)
.map(({domain, tld}) => <Col span={4}>
<Card bordered={false}> <Card bordered={false}>
<Statistic <Statistic
loading={stats === undefined} loading={stats === undefined}
title={`.${tld}`} prefix={<CompassOutlined/>}
value={domain} title={t`RDAP queries`}
valueStyle={{color: 'darkorange'}} value={stats?.rdapQueries}
/> />
</Card> </Card>
</Col>)} </Col>
</Row> <Col span={12}>
</> <Card bordered={false}>
} <Statistic
loading={stats === undefined}
title={t`Alerts sent`}
prefix={<NotificationOutlined/>}
value={stats?.alertSent}
valueStyle={{color: 'violet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain names in database`}
prefix={<DatabaseOutlined/>}
value={stats?.domainCountTotal}
valueStyle={{color: 'orange'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Tracked domain names`}
prefix={<AimOutlined/>}
value={stats?.domainTracked}
valueStyle={{color: 'violet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Purchased domain names`}
prefix={<FieldTimeOutlined/>}
value={stats?.domainPurchased}
valueStyle={{color: 'green'}}
/>
</Card>
</Col>
<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.`}
>
<Statistic
loading={stats === undefined}
title={t`Success rate`}
value={successRate === undefined ? '-' : successRate * 100}
suffix='%'
valueStyle={{color: successRate === undefined ? 'grey' : successRate >= 0.5 ? 'darkgreen' : 'orange'}}
/>
</Tooltip>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16} justify='center' align='middle'>
{stats?.domainCount
.sort((a, b) => b.domain - a.domain)
.map(({domain, tld}) => <Col key={tld} span={4}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={`.${tld}`}
value={domain}
valueStyle={{color: 'darkorange'}}
/>
</Card>
</Col>)}
</Row>
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,53 @@
import React, {useEffect, useState} from "react"; import React, {ReactElement, useEffect, useState} from 'react'
import {Collapse, Divider, Table, Typography} from "antd"; import {Collapse, Divider, Table, Typography} from 'antd'
import {getTldList, Tld} from "../../utils/api"; import {getTldList, Tld} from '../../utils/api'
import {t} from 'ttag' import {t} from 'ttag'
import {regionNames} from "../../i18n"; import {regionNames} from '../../i18n'
import useBreakpoint from "../../hooks/useBreakpoint"; import useBreakpoint from '../../hooks/useBreakpoint'
import {ColumnType} from "antd/es/table"; import {ColumnType} from 'antd/es/table'
import punycode from "punycode/punycode"; import punycode from 'punycode/punycode'
import {getCountryCode} from "../../utils/functions/getCountryCode"; import {getCountryCode} from '../../utils/functions/getCountryCode'
import {tldToEmoji} from "../../utils/functions/tldToEmoji"; import {tldToEmoji} from '../../utils/functions/tldToEmoji'
const {Text, Paragraph} = Typography const {Text, Paragraph} = Typography
type TldType = 'iTLD' | 'sTLD' | 'gTLD' | 'ccTLD' 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) { function TldTable(filters: FiltersType) {
interface TableRow {
key: string
TLD: ReactElement
Flag?: string
Country?: string
}
const sm = useBreakpoint('sm') const sm = useBreakpoint('sm')
const [dataTable, setDataTable] = useState<Tld[]>([]) const [dataTable, setDataTable] = useState<TableRow[]>([])
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const fetchData = (params: FiltersType & { page: number, itemsPerPage: number }) => { const fetchData = (params: FiltersType & { page: number, itemsPerPage: number }) => {
getTldList(params).then((data) => { getTldList(params).then((data) => {
setTotal(data['hydra:totalItems']) setTotal(data['hydra:totalItems'])
setDataTable(data['hydra:member'].map((tld: Tld) => { setDataTable(data['hydra:member'].map((tld: Tld) => {
const rowData = { const rowData = {
key: tld.tld, key: tld.tld,
TLD: <Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text> TLD: <Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text>
} }
switch (filters.type) { const type = filters.type
let countryName
switch (type) {
case 'ccTLD': case 'ccTLD':
let countryName
try { try {
countryName = regionNames.of(getCountryCode(tld.tld)) countryName = regionNames.of(getCountryCode(tld.tld))
} catch (e) { } catch {
countryName = '-' countryName = '-'
} }
@@ -60,98 +72,104 @@ function TldTable(filters: FiltersType) {
fetchData({...filters, page: 1, itemsPerPage: 30}) fetchData({...filters, page: 1, itemsPerPage: 30})
}, []) }, [])
let columns: ColumnType<any>[] = [ let columns: Array<ColumnType<TableRow>> = [
{ {
title: t`TLD`, title: t`TLD`,
dataIndex: "TLD" dataIndex: 'TLD'
} }
] ]
if (filters.type === 'ccTLD') columns = [...columns, { if (filters.type === 'ccTLD') {
title: t`Flag`, columns = [...columns, {
dataIndex: "Flag", title: t`Flag`,
}, { dataIndex: 'Flag'
title: t`Country`, }, {
dataIndex: "Country" title: t`Country`,
}] dataIndex: 'Country'
}]
}
if (filters.type === 'gTLD') columns = [...columns, { if (filters.type === 'gTLD') {
title: t`Registry Operator`, columns = [...columns, {
dataIndex: "Operator" title: t`Registry Operator`,
}] dataIndex: 'Operator'
}]
}
return (
<Table
columns={columns}
dataSource={dataTable}
pagination={{
total,
hideOnSinglePage: true,
defaultPageSize: 30,
onChange: (page, itemsPerPage) => {
fetchData({...filters, page, itemsPerPage})
}
}}
return <Table {...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
columns={columns} />
dataSource={dataTable} )
pagination={{
total,
hideOnSinglePage: true,
defaultPageSize: 30,
onChange: (page, itemsPerPage) => {
fetchData({...filters, page, itemsPerPage})
}
}}
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
} }
export default function TldPage() { export default function TldPage() {
const sm = useBreakpoint('sm') const sm = useBreakpoint('sm')
return <> return (
<Paragraph> <>
{t`This page presents all active TLDs in the root zone database.`} <Paragraph>
</Paragraph> {t`This page presents all active TLDs in the root zone database.`}
<Paragraph> </Paragraph>
{t`IANA provides the list of currently active TLDs, regardless of their type, and ICANN provides the list of gTLDs. <Paragraph>
{t`IANA provides the list of currently active TLDs, regardless of their type, and ICANN provides the list of gTLDs.
In most cases, the two-letter ccTLD assigned to a country is made in accordance with the ISO 3166-1 standard. In most cases, the two-letter ccTLD assigned to a country is made in accordance with the ISO 3166-1 standard.
This data is updated every month. Three HTTP requests are needed for the complete update of TLDs in Domain Watchdog (two requests to IANA and one to ICANN). This data is updated every month. Three HTTP requests are needed for the complete update of TLDs in Domain Watchdog (two requests to IANA and one to ICANN).
At the same time, the list of root RDAP servers is updated.`} At the same time, the list of root RDAP servers is updated.`}
</Paragraph> </Paragraph>
<Divider/> <Divider/>
<Collapse <Collapse
accordion accordion
size={sm ? 'small' : 'large'} size={sm ? 'small' : 'large'}
items={[ items={[
{ {
key: 'sTLD', key: 'sTLD',
label: t`Sponsored Top-Level-Domains`, label: t`Sponsored Top-Level-Domains`,
children: <> children: <>
<Text>{t`Top-level domains sponsored by specific organizations that set rules for registration and use, often related to particular interest groups or industries.`}</Text> <Text>{t`Top-level domains sponsored by specific organizations that set rules for registration and use, often related to particular interest groups or industries.`}</Text>
<Divider/> <Divider/>
<TldTable type='sTLD'/> <TldTable type='sTLD'/>
</> </>
}, },
{ {
key: 'gTLD', key: 'gTLD',
label: t`Generic Top-Level-Domains`, label: t`Generic Top-Level-Domains`,
children: <> children: <>
<Text>{t`Generic top-level domains open to everyone, not restricted by specific criteria, representing various themes or industries.`}</Text> <Text>{t`Generic top-level domains open to everyone, not restricted by specific criteria, representing various themes or industries.`}</Text>
<Divider/> <Divider/>
<TldTable type='gTLD' contractTerminated={false} specification13={false}/> <TldTable type='gTLD' contractTerminated={false} specification13={false}/>
</> </>
}, },
{ {
key: 'ngTLD', key: 'ngTLD',
label: t`Brand Generic Top-Level-Domains`, label: t`Brand Generic Top-Level-Domains`,
children: <> children: <>
<Text>{t`Generic top-level domains associated with specific brands, allowing companies to use their own brand names as domains.`}</Text> <Text>{t`Generic top-level domains associated with specific brands, allowing companies to use their own brand names as domains.`}</Text>
<Divider/> <Divider/>
<TldTable type='gTLD' contractTerminated={false} specification13={true}/> <TldTable type='gTLD' contractTerminated={false} specification13/>
</> </>
}, },
{ {
key: 'ccTLD', key: 'ccTLD',
label: t`Country-Code Top-Level-Domains`, label: t`Country-Code Top-Level-Domains`,
children: <> children: <>
<Text>{t`Top-level domains based on country codes, identifying websites according to their country of origin.`}</Text> <Text>{t`Top-level domains based on country codes, identifying websites according to their country of origin.`}</Text>
<Divider/><TldTable type='ccTLD'/> <Divider/><TldTable type='ccTLD'/>
</> </>
} }
]} ]}
/> />
</> </>
} )
}

View File

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

View File

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

View File

@@ -1,34 +1,19 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from 'react'
import {Card, Divider, Flex, Form, message} from "antd"; import {Card, Divider, Flex, Form, message} from 'antd'
import {EventAction, getWatchlists, postWatchlist, putWatchlist} from "../../utils/api"; import {getWatchlists, postWatchlist, putWatchlist, Watchlist} from '../../utils/api'
import {AxiosError} from "axios"; import {AxiosError} from 'axios'
import {t} from 'ttag' import {t} from 'ttag'
import {WatchlistForm} from "../../components/tracking/watchlist/WatchlistForm"; import {WatchlistForm} from '../../components/tracking/watchlist/WatchlistForm'
import {WatchlistsList} from "../../components/tracking/watchlist/WatchlistsList"; import {WatchlistsList} from '../../components/tracking/watchlist/WatchlistsList'
import {Connector, getConnectors} from "../../utils/api/connectors"; import {Connector, getConnectors} from '../../utils/api/connectors'
import {showErrorAPI} from "../../utils/functions/showErrorAPI"; import {showErrorAPI} from '../../utils/functions/showErrorAPI'
interface FormValuesType {
export type Watchlist = {
name?: string name?: string
token: string, domains: 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[],
triggers: string[] triggers: string[]
connector?: string, connector?: string
dsn?: string[] dsn?: string[]
} }
@@ -52,14 +37,13 @@ const getRequestDataFromForm = (values: FormValuesType) => {
} }
export default function WatchlistPage() { export default function WatchlistPage() {
const [form] = Form.useForm() const [form] = Form.useForm()
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const [watchlists, setWatchlists] = useState<Watchlist[]>() const [watchlists, setWatchlists] = useState<Watchlist[]>()
const [connectors, setConnectors] = useState<(Connector & { id: string })[]>() const [connectors, setConnectors] = useState<Array<Connector & { id: string }>>()
const onCreateWatchlist = (values: FormValuesType) => { const onCreateWatchlist = (values: FormValuesType) => {
postWatchlist(getRequestDataFromForm(values)).then((w) => { postWatchlist(getRequestDataFromForm(values)).then(() => {
form.resetFields() form.resetFields()
refreshWatchlists() refreshWatchlists()
messageApi.success(t`Watchlist created !`) 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, token: values.token,
...getRequestDataFromForm(values) ...getRequestDataFromForm(values)
} }
).then((w) => { ).then(() => {
refreshWatchlists() refreshWatchlists()
messageApi.success(t`Watchlist updated !`) messageApi.success(t`Watchlist updated !`)
}).catch((e: AxiosError) => { }).catch((e: AxiosError) => {
throw showErrorAPI(e, messageApi) throw showErrorAPI(e, messageApi)
}) })
const refreshWatchlists = () => getWatchlists().then(w => { const refreshWatchlists = async () => await getWatchlists().then(w => {
setWatchlists(w['hydra:member']) setWatchlists(w['hydra:member'])
}).catch((e: AxiosError) => { }).catch((e: AxiosError) => {
setWatchlists(undefined) setWatchlists(undefined)
@@ -95,18 +79,20 @@ export default function WatchlistPage() {
}) })
}, []) }, [])
return <Flex gap="middle" align="center" justify="center" vertical> return (
{contextHolder} <Flex gap='middle' align='center' justify='center' vertical>
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}> {contextHolder}
{connectors && <Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation={true}/> {(connectors != null) &&
} <WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation/>}
</Card> </Card>
<Divider/> <Divider/>
{connectors && watchlists && watchlists.length > 0 && {(connectors != null) && (watchlists != null) && watchlists.length > 0 &&
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists} <WatchlistsList
connectors={connectors} watchlists={watchlists} onDelete={refreshWatchlists}
onUpdateWatchlist={onUpdateWatchlist} connectors={connectors}
/>} onUpdateWatchlist={onUpdateWatchlist}
</Flex> />}
} </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 { export enum ConnectorProvider {
OVH = 'ovh', OVH = 'ovh',
@@ -7,13 +8,18 @@ export enum ConnectorProvider {
NAMECHEAP = 'namecheap' NAMECHEAP = 'namecheap'
} }
export type Connector = { export interface Connector {
provider: ConnectorProvider provider: ConnectorProvider
authData: object authData: object
} }
export async function getConnectors() { interface ConnectorResponse {
const response = await request({ 'hydra:totalItems': number
'hydra:member': ConnectorElement[]
}
export async function getConnectors(): Promise<ConnectorResponse> {
const response = await request<ConnectorResponse>({
url: 'connectors' url: 'connectors'
}) })
return response.data return response.data
@@ -25,7 +31,7 @@ export async function postConnector(connector: Connector) {
url: 'connectors', url: 'connectors',
data: connector, data: connector,
headers: { headers: {
"Content-Type": 'application/json' 'Content-Type': 'application/json'
} }
}) })
return response.data 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> { export async function getDomain(ldhName: string): Promise<Domain> {
const response = await request<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 = export type EventAction =
'registration' 'registration'
@@ -26,7 +25,12 @@ export interface Event {
export interface Entity { export interface Entity {
handle: string handle: string
jCard: any jCard: ['vcard', Array<[
string,
{ [key: string]: string | string[] },
string,
string | string[],
]>] | []
} }
export interface Nameserver { export interface Nameserver {
@@ -50,12 +54,12 @@ export interface Domain {
handle: string handle: string
status: string[] status: string[]
events: Event[] events: Event[]
entities: { entities: Array<{
entity: Entity entity: Entity
events: Event[] events: Event[]
roles: string[] roles: string[]
deleted: boolean deleted: boolean
}[] }>
nameservers: Nameserver[] nameservers: Nameserver[]
tld: Tld tld: Tld
deleted: boolean deleted: boolean
@@ -70,20 +74,24 @@ export interface User {
export interface WatchlistRequest { export interface WatchlistRequest {
name?: string name?: string
domains: string[], domains: string[]
triggers: { event: EventAction, action: TriggerAction }[], triggers: Array<{ event: EventAction, action: TriggerAction }>
connector?: string connector?: string
dsn?: string[] dsn?: string[]
} }
export interface Watchlist { export interface Watchlist {
token: string
name?: string name?: string
domains: Domain[], token: string
triggers: { event: EventAction, action: TriggerAction }[], domains: Domain[]
connector?: string triggers?: Array<{ event: EventAction, action: string }>
createdAt: string
dsn?: string[] dsn?: string[]
connector?: {
id: string
provider: string
createdAt: string
}
createdAt: string
} }
export interface InstanceConfig { export interface InstanceConfig {
@@ -97,28 +105,30 @@ export interface Statistics {
alertSent: number alertSent: number
domainPurchased: number domainPurchased: number
domainPurchaseFailed: number domainPurchaseFailed: number
domainCount: {tld: string, domain: number}[] domainCount: Array<{ tld: string, domain: number }>
domainCountTotal: number domainCountTotal: number
domainTracked: 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 = { const axiosConfig: AxiosRequestConfig = {
...config, ...config,
baseURL: '/api', baseURL: '/api',
withCredentials: true, withCredentials: true,
headers: { headers: {
Accept: 'application/ld+json', Accept: 'application/ld+json',
...config.headers, ...config.headers
} }
} }
return await axios.request<T, R, D>(axiosConfig) return await axios.request<T, R, D>(axiosConfig)
} }
export * from './domain' export * from './domain'
export * from './tld' export * from './tld'
export * from './user' export * from './user'
export * from './watchlist' export * from './watchlist'

View File

@@ -1,15 +1,13 @@
import {request} from "./index"; import {request, Tld} from './index'
interface Tld { interface TldList {
tld: string 'hydra:totalItems': number
contractTerminated: boolean 'hydra:member': Tld[]
registryOperator: string
specification13: boolean
} }
export async function getTldList(params: object): Promise<any> { export async function getTldList(params: object): Promise<TldList> {
return (await request<Tld[]>({ return (await request<TldList>({
url: 'tld', url: 'tld',
params, params
})).data })).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> { export async function login(email: string, password: string): Promise<boolean> {
const response = await request({ const response = await request({
@@ -19,7 +18,6 @@ export async function register(email: string, password: string): Promise<boolean
return response.status === 201 return response.status === 201
} }
export async function getUser(): Promise<User> { export async function getUser(): Promise<User> {
const response = await request<User>({ const response = await request<User>({
url: 'me' url: 'me'
@@ -39,4 +37,4 @@ export async function getStatistics(): Promise<Statistics> {
url: 'stats' url: 'stats'
}) })
return response.data return response.data
} }

View File

@@ -1,7 +1,12 @@
import {Domain, request, Watchlist, WatchlistRequest} from "./index"; import {request, TrackedDomains, Watchlist, WatchlistRequest} from './index'
export async function getWatchlists() { interface WatchlistList {
const response = await request({ 'hydra:totalItems': number
'hydra:member': Watchlist[]
}
export async function getWatchlists(): Promise<WatchlistList> {
const response = await request<WatchlistList>({
url: 'watchlists' url: 'watchlists'
}) })
return response.data return response.data
@@ -20,7 +25,7 @@ export async function postWatchlist(watchlist: WatchlistRequest) {
url: 'watchlists', url: 'watchlists',
data: watchlist, data: watchlist,
headers: { headers: {
"Content-Type": 'application/json' 'Content-Type': 'application/json'
} }
}) })
return response.data return response.data
@@ -37,17 +42,16 @@ export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { toke
const response = await request<WatchlistRequest>({ const response = await request<WatchlistRequest>({
method: 'PUT', method: 'PUT',
url: 'watchlists/' + watchlist.token, url: 'watchlists/' + watchlist.token,
data: watchlist, data: watchlist
}) })
return response.data return response.data
} }
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<any> { export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<TrackedDomains> {
const response = await request({ const response = await request<TrackedDomains>({
method: 'GET', method: 'GET',
url: 'tracked', url: 'tracked',
params params
}) })
return response.data 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' : export const actionToColor = (a: EventAction) => a === 'registration'
a === 'reregistration' ? 'cyan' : ? 'green'
a === 'expiration' ? 'red' : : a === 'reregistration'
a === 'deletion' ? 'magenta' : ? 'cyan'
a === 'transfer' ? 'orange' : : a === 'expiration'
a === 'last changed' ? 'blue' : ? 'red'
a === 'registrar expiration' ? 'red' : : a === 'deletion'
a === 'reinstantiation' ? 'purple' : ? 'magenta'
a === 'enum validation expiration' ? 'red' : 'default' : 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 { import {
ClockCircleOutlined, ClockCircleOutlined,
DeleteOutlined, DeleteOutlined,
@@ -9,20 +9,31 @@ import {
SignatureOutlined, SignatureOutlined,
SyncOutlined, SyncOutlined,
UnlockOutlined UnlockOutlined
} from "@ant-design/icons"; } from '@ant-design/icons'
import React from "react"; import React from 'react'
export const actionToIcon = (a: EventAction) => a === 'registration' ? export const actionToIcon = (a: EventAction) => a === 'registration'
<SignatureOutlined style={{fontSize: '16px'}}/> : a === 'expiration' ? ? <SignatureOutlined style={{fontSize: '16px'}}/>
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'transfer' ? : a === 'expiration'
<ShareAltOutlined style={{fontSize: '16px'}}/> : a === 'last changed' ? ? <ClockCircleOutlined style={{fontSize: '16px'}}/>
<SyncOutlined style={{fontSize: '16px'}}/> : a === 'deletion' ? : a === 'transfer'
<DeleteOutlined style={{fontSize: '16px'}}/> : a === 'reregistration' ? ? <ShareAltOutlined style={{fontSize: '16px'}}/>
<ReloadOutlined style={{fontSize: '16px'}}/> : a === 'locked' ? : a === 'last changed'
<LockOutlined style={{fontSize: '16px'}}/> : a === 'unlocked' ? ? <SyncOutlined style={{fontSize: '16px'}}/>
<UnlockOutlined style={{fontSize: '16px'}}/> : a === 'registrar expiration' ? : a === 'deletion'
<ClockCircleOutlined ? <DeleteOutlined style={{fontSize: '16px'}}/>
style={{fontSize: '16px'}}/> : a === 'enum validation expiration' ? : a === 'reregistration'
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'reinstantiation' ? ? <ReloadOutlined style={{fontSize: '16px'}}/>
<ReloadOutlined style={{fontSize: '16px'}}/> : : a === 'locked'
<PushpinOutlined style={{fontSize: '16px'}}/> ? <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 {Entity} from '../api'
import vCard from "vcf"; import vCard from 'vcf'
export const entityToName = (e: { entity: Entity }): string => { export const entityToName = (e: { entity: Entity }): string => {
if (e.entity.jCard.length === 0) return e.entity.handle if (e.entity.jCard.length === 0) return e.entity.handle
@@ -10,4 +10,4 @@ export const entityToName = (e: { entity: Entity }): string => {
if (jCard.data.fn && !Array.isArray(jCard.data.fn) && jCard.data.fn.valueOf() !== '') name = jCard.data.fn.valueOf() if (jCard.data.fn && !Array.isArray(jCard.data.fn) && jCard.data.fn.valueOf() !== '') name = jCard.data.fn.valueOf()
return name return name
} }

View File

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

View File

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

View File

@@ -2,4 +2,4 @@ export const getCountryCode = (tld: string): string => {
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'} const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions] if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
return tld.toUpperCase() return tld.toUpperCase()
} }

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 * @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
@@ -17,7 +17,6 @@ export const rdapRoleTranslation = () => ({
noc: t`Noc` noc: t`Noc`
}) })
/** /**
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml * @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).` 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 * @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 * @see https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
*/ */
export const rdapStatusCodeDetailTranslation = () => ({ 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.`, 'renew prohibited': t`Renewal or reregistration of the object instance is forbidden.`,
'update prohibited': t`Updates to the object instance are 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.`, '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.`, '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.`, 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.`, 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.`, 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.`, 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.`, 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.`, 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.`, 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.`, 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 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 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.`, '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.`, '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.`, '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.`, '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 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 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.`, '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.`, '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.`, '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).`, 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.`, 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 { import {
BankOutlined, BankOutlined,
DollarOutlined, DollarOutlined,
@@ -6,20 +6,22 @@ import {
SignatureOutlined, SignatureOutlined,
ToolOutlined, ToolOutlined,
UserOutlined UserOutlined
} from "@ant-design/icons"; } from '@ant-design/icons'
import React from "react"; import React from 'react'
import {rolesToColor} from "./rolesToColor"; import {rolesToColor} from './rolesToColor'
export const roleToAvatar = (e: { roles: string[] }) => <Avatar style={{backgroundColor: rolesToColor(e.roles)}} export const roleToAvatar = (e: { roles: string[] }) => <Avatar
icon={e.roles.includes('registrant') ? style={{backgroundColor: rolesToColor(e.roles)}}
<SignatureOutlined/> : icon={e.roles.includes('registrant')
e.roles.includes('registrar') ? ? <SignatureOutlined/>
<BankOutlined/> : : e.roles.includes('registrar')
e.roles.includes('administrative') ? ? <BankOutlined/>
<IdcardOutlined/> : : e.roles.includes('administrative')
e.roles.includes('technical') ? ? <IdcardOutlined/>
<ToolOutlined/> : : e.roles.includes('technical')
e.roles.includes('billing') ? ? <ToolOutlined/>
<DollarOutlined/> : : e.roles.includes('billing')
<UserOutlined/>}/> ? <DollarOutlined/>
: <UserOutlined/>}
/>

View File

@@ -1,6 +1,11 @@
export const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' : export const rolesToColor = (roles: string[]) => roles.includes('registrant')
roles.includes('registrar') ? 'purple' : ? 'green'
roles.includes('administrative') ? 'blue' : : roles.includes('registrar')
roles.includes('technical') ? 'orange' : ? 'purple'
roles.includes('sponsor') ? 'magenta' : : roles.includes('administrative')
roles.includes('billing') ? 'cyan' : 'default' ? '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 {AxiosError, AxiosResponse} from 'axios'
import {MessageInstance, MessageType} from "antd/lib/message/interface"; import {MessageInstance, MessageType} from 'antd/lib/message/interface'
import {t} from "ttag"; import {t} from 'ttag'
export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined { export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined {
const response = e.response as AxiosResponse const response = e.response as AxiosResponse
const data = response.data const data = response.data
@@ -24,4 +23,4 @@ export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): Messag
} }
return messageApi.error(detail !== '' ? detail : t`An error occurred`) return messageApi.error(detail !== '' ? detail : t`An error occurred`)
} }

View File

@@ -1,11 +1,14 @@
import {Domain} from "../api"; import {Domain} from '../api'
export const sortDomainEntities = (domain: Domain) => domain.entities export const sortDomainEntities = (domain: Domain) => domain.entities
.filter(e => !e.deleted) .filter(e => !e.deleted)
.sort((e1, e2) => { .sort((e1, e2) => {
const p = (r: string[]) => r.includes('registrant') ? 5 : const p = (r: string[]) => r.includes('registrant')
r.includes('administrative') ? 4 : ? 5
r.includes('billing') ? 3 : : r.includes('administrative')
r.includes('registrar') ? 2 : 1 ? 4
: r.includes('billing')
? 3
: r.includes('registrar') ? 2 : 1
return p(e2.roles) - p(e1.roles) 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) => { export const tldToEmoji = (tld: string) => {
if (tld.startsWith('xn--')) return '-' if (tld.startsWith('xn--')) return '-'
@@ -9,4 +9,4 @@ export const tldToEmoji = (tld: string) => {
.split('') .split('')
.map((char) => 127397 + char.charCodeAt(0)) .map((char) => 127397 + char.charCodeAt(0))
) )
} }

View File

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

View File

@@ -1,5 +1,5 @@
import {t} from "ttag"; import {t} from 'ttag'
import {regionNames} from "../../i18n"; import {regionNames} from '../../i18n'
export const ovhFields = () => ({ export const ovhFields = () => ({
appKey: t`Application key`, appKey: t`Application key`,
@@ -24,4 +24,4 @@ export const ovhPricingMode = () => [
value: 'create-premium', value: 'create-premium',
label: t`The domain is free but can be premium. Its price varies from one domain to another` label: t`The domain is free but can be premium. Its price varies from one domain to another`
} }
] ]

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

1480
yarn.lock

File diff suppressed because it is too large Load Diff