mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: add eslint linter
This commit is contained in:
7
.github/workflows/symfony.yml
vendored
7
.github/workflows/symfony.yml
vendored
@@ -23,10 +23,15 @@ jobs:
|
||||
extensions: mbstring, xml, intl, curl, iconv, pdo_pgsql, sodium, zip, http
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
|
||||
run: >
|
||||
composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
|
||||
yarn install
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
|
||||
- name: Run PHPStan
|
||||
run: vendor/bin/phpstan analyse
|
||||
|
||||
- name: Run ESLint
|
||||
run: yarn run eslint
|
||||
|
||||
206
assets/App.tsx
206
assets/App.tsx
@@ -1,23 +1,23 @@
|
||||
import {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, Typography} from "antd";
|
||||
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from "react-router-dom";
|
||||
import TextPage from "./pages/TextPage";
|
||||
import DomainSearchPage from "./pages/search/DomainSearchPage";
|
||||
import EntitySearchPage from "./pages/search/EntitySearchPage";
|
||||
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
|
||||
import TldPage from "./pages/search/TldPage";
|
||||
import StatisticsPage from "./pages/StatisticsPage";
|
||||
import WatchlistPage from "./pages/tracking/WatchlistPage";
|
||||
import UserPage from "./pages/UserPage";
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {getUser} from "./utils/api";
|
||||
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
|
||||
import ConnectorPage from "./pages/tracking/ConnectorPage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
import useBreakpoint from "./hooks/useBreakpoint";
|
||||
import {Sider} from "./components/Sider";
|
||||
import {jt, t} from "ttag";
|
||||
import {Button, ConfigProvider, FloatButton, Layout, Space, theme, Tooltip, Typography} from 'antd'
|
||||
import {Link, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
|
||||
import TextPage from './pages/TextPage'
|
||||
import DomainSearchPage from './pages/search/DomainSearchPage'
|
||||
import EntitySearchPage from './pages/search/EntitySearchPage'
|
||||
import NameserverSearchPage from './pages/search/NameserverSearchPage'
|
||||
import TldPage from './pages/search/TldPage'
|
||||
import StatisticsPage from './pages/StatisticsPage'
|
||||
import WatchlistPage from './pages/tracking/WatchlistPage'
|
||||
import UserPage from './pages/UserPage'
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {getUser} from './utils/api'
|
||||
import LoginPage, {AuthenticatedContext} from './pages/LoginPage'
|
||||
import ConnectorPage from './pages/tracking/ConnectorPage'
|
||||
import NotFoundPage from './pages/NotFoundPage'
|
||||
import useBreakpoint from './hooks/useBreakpoint'
|
||||
import {Sider} from './components/Sider'
|
||||
import {jt, t} from 'ttag'
|
||||
import {BugOutlined, InfoCircleOutlined, MergeOutlined} from '@ant-design/icons'
|
||||
import TrackedDomainPage from "./pages/tracking/TrackedDomainPage";
|
||||
import TrackedDomainPage from './pages/tracking/TrackedDomainPage'
|
||||
|
||||
const PROJECT_LINK = 'https://github.com/maelgangloff/domain-watchdog'
|
||||
const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
|
||||
@@ -25,35 +25,33 @@ const LICENSE_LINK = 'https://www.gnu.org/licenses/agpl-3.0.txt'
|
||||
const ProjectLink = <Typography.Link target='_blank' href={PROJECT_LINK}>Domain Watchdog</Typography.Link>
|
||||
const LicenseLink = <Typography.Link target='_blank' href={LICENSE_LINK}>AGPL-3.0-or-later</Typography.Link>
|
||||
|
||||
export default function App() {
|
||||
|
||||
export default function App(): React.ReactElement {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
|
||||
const authenticated = useCallback((authenticated: boolean) => {
|
||||
setIsAuthenticated(authenticated)
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
authenticated,
|
||||
setIsAuthenticated
|
||||
}), [authenticated, setIsAuthenticated])
|
||||
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
|
||||
const windowQuery = window.matchMedia("(prefers-color-scheme:dark)");
|
||||
const windowQuery = window.matchMedia('(prefers-color-scheme:dark)')
|
||||
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
|
||||
setDarkMode(event.matches)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
windowQuery.addEventListener("change", darkModeChange)
|
||||
windowQuery.addEventListener('change', darkModeChange)
|
||||
return () => {
|
||||
windowQuery.removeEventListener("change", darkModeChange)
|
||||
windowQuery.removeEventListener('change', darkModeChange)
|
||||
}
|
||||
}, [windowQuery, darkModeChange])
|
||||
|
||||
@@ -69,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
|
||||
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'/>}/>
|
||||
<Route path='/search/domain' element={<DomainSearchPage/>}/>
|
||||
<Route path='/search/domain/:query' element={<DomainSearchPage/>}/>
|
||||
<Route path='/search/entity' element={<EntitySearchPage/>}/>
|
||||
<Route path='/search/nameserver' element={<NameserverSearchPage/>}/>
|
||||
<Route path='/search/tld' element={<TldPage/>}/>
|
||||
|
||||
<Route path="/search/domain" element={<DomainSearchPage/>}/>
|
||||
<Route path="/search/domain/:query" element={<DomainSearchPage/>}/>
|
||||
<Route path="/search/entity" element={<EntitySearchPage/>}/>
|
||||
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
|
||||
<Route path="/search/tld" element={<TldPage/>}/>
|
||||
<Route path='/tracking/watchlist' element={<WatchlistPage/>}/>
|
||||
<Route path='/tracking/domains' element={<TrackedDomainPage/>}/>
|
||||
<Route path='/tracking/connectors' element={<ConnectorPage/>}/>
|
||||
|
||||
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
|
||||
<Route path="/tracking/domains" element={<TrackedDomainPage/>}/>
|
||||
<Route path="/tracking/connectors" element={<ConnectorPage/>}/>
|
||||
<Route path='/stats' element={<StatisticsPage/>}/>
|
||||
<Route path='/user' element={<UserPage/>}/>
|
||||
|
||||
<Route path="/stats" element={<StatisticsPage/>}/>
|
||||
<Route path="/user" element={<UserPage/>}/>
|
||||
<Route path='/faq' element={<TextPage resource='faq.md'/>}/>
|
||||
<Route path='/tos' element={<TextPage resource='tos.md'/>}/>
|
||||
<Route path='/privacy' element={<TextPage resource='privacy.md'/>}/>
|
||||
|
||||
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
|
||||
<Route path="/tos" element={<TextPage resource='tos.md'/>}/>
|
||||
<Route path="/privacy" element={<TextPage resource='privacy.md'/>}/>
|
||||
<Route path='/login' element={<LoginPage/>}/>
|
||||
|
||||
<Route path="/login" element={<LoginPage/>}/>
|
||||
|
||||
<Route path="*" element={<NotFoundPage/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
<Layout.Footer style={{textAlign: 'center'}}>
|
||||
<Space size='middle' wrap align='center'>
|
||||
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
|
||||
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
||||
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
||||
<Typography.Link target='_blank'
|
||||
href='https://github.com/maelgangloff/domain-watchdog/wiki'><Button
|
||||
type='text'>{t`Documentation`}</Button></Typography.Link>
|
||||
</Space>
|
||||
<Typography.Paragraph style={{marginTop: '1em'}}>
|
||||
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
|
||||
</Typography.Paragraph>
|
||||
</Layout.Footer>
|
||||
<Route path='*' element={<NotFoundPage/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
<Layout.Footer style={{textAlign: 'center'}}>
|
||||
<Space size='middle' wrap align='center'>
|
||||
<Link to='/tos'><Button type='text'>{t`TOS`}</Button></Link>
|
||||
<Link to='/privacy'><Button type='text'>{t`Privacy Policy`}</Button></Link>
|
||||
<Link to='/faq'><Button type='text'>{t`FAQ`}</Button></Link>
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href='https://github.com/maelgangloff/domain-watchdog/wiki'
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
>{t`Documentation`}
|
||||
</Button>
|
||||
</Typography.Link>
|
||||
</Space>
|
||||
<Typography.Paragraph style={{marginTop: '1em'}}>
|
||||
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
|
||||
</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>
|
||||
<FloatButton.Group
|
||||
trigger='hover'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
insetInlineEnd: (100 - 40) / 2,
|
||||
bottom: 100 - 40 / 2,
|
||||
}}
|
||||
icon={<InfoCircleOutlined/>}
|
||||
>
|
||||
<Tooltip title={t`Official git repository`} placement='left'>
|
||||
<FloatButton icon={<MergeOutlined/>} target='_blank' href={PROJECT_LINK}/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t`Submit an issue`} placement='left'>
|
||||
<FloatButton icon={<BugOutlined/>} target='_blank' href={PROJECT_LINK + '/issues'}/>
|
||||
</Tooltip>
|
||||
</FloatButton.Group>
|
||||
</Layout>
|
||||
</AuthenticatedContext.Provider>
|
||||
</ConfigProvider>
|
||||
}
|
||||
</AuthenticatedContext.Provider>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import {Button, Form, Input, message, Space} from "antd";
|
||||
import {t} from "ttag";
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {getUser, login} from "../utils/api";
|
||||
import {AuthenticatedContext} from "../pages/LoginPage";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Button, Form, Input, message, Space} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React, {useContext, useEffect} from 'react'
|
||||
import {getUser, login} from '../utils/api'
|
||||
import {AuthenticatedContext} from '../pages/LoginPage'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
|
||||
import {showErrorAPI} from "../utils/functions/showErrorAPI";
|
||||
import {showErrorAPI} from '../utils/functions/showErrorAPI'
|
||||
|
||||
|
||||
type FieldType = {
|
||||
username: string;
|
||||
password: string;
|
||||
interface FieldType {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const {setIsAuthenticated} = useContext(AuthenticatedContext)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(() => {
|
||||
setIsAuthenticated(true)
|
||||
navigate('/home')
|
||||
})
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
const onFinish = (data: FieldType) => {
|
||||
login(data.username, data.password).then(() => {
|
||||
setIsAuthenticated(true)
|
||||
@@ -38,44 +33,46 @@ export function LoginForm({ssoLogin}: { ssoLogin?: boolean }) {
|
||||
showErrorAPI(e, messageApi)
|
||||
})
|
||||
}
|
||||
return <>
|
||||
{contextHolder}
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{span: 8}}
|
||||
wrapperCol={{span: 16}}
|
||||
style={{maxWidth: 600}}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label={t`Email address`}
|
||||
name="username"
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Form
|
||||
name='basic'
|
||||
labelCol={{span: 8}}
|
||||
wrapperCol={{span: 16}}
|
||||
style={{maxWidth: 600}}
|
||||
onFinish={onFinish}
|
||||
autoComplete='off'
|
||||
>
|
||||
<Input autoFocus/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label={t`Password`}
|
||||
name="password"
|
||||
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
|
||||
label={t`Email address`}
|
||||
name='username'
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoFocus/>
|
||||
</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>
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label={t`Password`}
|
||||
name='password'
|
||||
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>
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import {Button, Form, Input, message} from "antd";
|
||||
import {t} from "ttag";
|
||||
import React, {useState} from "react";
|
||||
import {register} from "../utils/api";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Button, Form, Input, message} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React from 'react'
|
||||
import {register} from '../utils/api'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
|
||||
import {showErrorAPI} from "../utils/functions/showErrorAPI";
|
||||
import {showErrorAPI} from '../utils/functions/showErrorAPI'
|
||||
|
||||
|
||||
type FieldType = {
|
||||
username: string;
|
||||
password: string;
|
||||
interface FieldType {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export function RegisterForm() {
|
||||
|
||||
const [error, setError] = useState<string>()
|
||||
const navigate = useNavigate()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
@@ -25,37 +22,39 @@ export function RegisterForm() {
|
||||
showErrorAPI(e, messageApi)
|
||||
})
|
||||
}
|
||||
return <>
|
||||
{contextHolder}
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{span: 8}}
|
||||
wrapperCol={{span: 16}}
|
||||
style={{maxWidth: 600}}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label={t`Email address`}
|
||||
name="username"
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Form
|
||||
name='basic'
|
||||
labelCol={{span: 8}}
|
||||
wrapperCol={{span: 16}}
|
||||
style={{maxWidth: 600}}
|
||||
onFinish={onFinish}
|
||||
autoComplete='off'
|
||||
>
|
||||
<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>
|
||||
label={t`Password`}
|
||||
name="password"
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item<FieldType>
|
||||
label={t`Password`}
|
||||
name='password'
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{offset: 8, span: 16}}>
|
||||
<Button block type="primary" htmlType="submit">
|
||||
{t`Register`}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
}
|
||||
<Form.Item wrapperCol={{offset: 8, span: 16}}>
|
||||
<Button block type='primary' htmlType='submit'>
|
||||
{t`Register`}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ItemType} from "antd/lib/menu/interface";
|
||||
import {t} from "ttag";
|
||||
import {ItemType} from 'antd/lib/menu/interface'
|
||||
import {t} from 'ttag'
|
||||
import {
|
||||
AimOutlined,
|
||||
ApiOutlined,
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
SearchOutlined,
|
||||
TableOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {Menu} from "antd";
|
||||
import React from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
} from '@ant-design/icons'
|
||||
import {Menu} from 'antd'
|
||||
import React from 'react'
|
||||
import {useLocation, useNavigate} from 'react-router-dom'
|
||||
|
||||
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
const navigate = useNavigate()
|
||||
@@ -49,25 +49,25 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
title: t`TLD list`,
|
||||
disabled: !isAuthenticated,
|
||||
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/>,
|
||||
label: t`Log out`,
|
||||
danger: true,
|
||||
onClick: () => window.location.replace("/logout")
|
||||
onClick: () => window.location.replace('/logout')
|
||||
}])
|
||||
} else {
|
||||
menuItems.push({
|
||||
@@ -129,12 +129,13 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
||||
})
|
||||
}
|
||||
|
||||
return <Menu
|
||||
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
|
||||
selectedKeys={[location.pathname.includes('/search/domain') ? '/search/domain' : location.pathname]}
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
items={menuItems}
|
||||
/>
|
||||
|
||||
}
|
||||
return (
|
||||
<Menu
|
||||
defaultOpenKeys={['search', 'info', 'tracking', 'doc']}
|
||||
selectedKeys={[location.pathname.includes('/search/domain') ? '/search/domain' : location.pathname]}
|
||||
mode='inline'
|
||||
theme='dark'
|
||||
items={menuItems}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, {useEffect} from "react";
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
|
||||
import {Flex} from "antd";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {getLayoutedElements} from "../tracking/watchlist/diagram/getLayoutedElements";
|
||||
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from "../tracking/watchlist/diagram/watchlistToNodes";
|
||||
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from "../tracking/watchlist/diagram/watchlistToEdges";
|
||||
import React, {useEffect} from 'react'
|
||||
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
|
||||
import {Flex} from 'antd'
|
||||
import {Domain} from '../../utils/api'
|
||||
import {getLayoutedElements} from '../tracking/watchlist/diagram/getLayoutedElements'
|
||||
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from '../tracking/watchlist/diagram/watchlistToNodes'
|
||||
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from '../tracking/watchlist/diagram/watchlistToEdges'
|
||||
|
||||
export function DomainDiagram({domain}: { domain: Domain }) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
useEffect(() => {
|
||||
const nodes = [
|
||||
@@ -33,21 +32,23 @@ export function DomainDiagram({domain}: { domain: Domain }) {
|
||||
setEdges(e.edges)
|
||||
}, [])
|
||||
|
||||
return <Flex style={{width: '100%', height: '100vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='system'
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
}
|
||||
return (
|
||||
<Flex style={{width: '100%', height: '100vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='system'
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import {StepProps, Steps, Tooltip} from "antd";
|
||||
import React from "react";
|
||||
import {t} from "ttag";
|
||||
import {StepProps, Steps, Tooltip} from 'antd'
|
||||
import React from 'react'
|
||||
import {t} from 'ttag'
|
||||
import {
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
SignatureOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
|
||||
} from '@ant-design/icons'
|
||||
import {rdapEventDetailTranslation, rdapStatusCodeDetailTranslation} from '../../utils/functions/rdapTranslation'
|
||||
|
||||
export function DomainLifecycleSteps({status}: { status: string[] }) {
|
||||
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
|
||||
const steps: StepProps[] = [
|
||||
{
|
||||
title: <Tooltip title={rdapEventDetailTranslated.registration}>{t`Registration`}</Tooltip>,
|
||||
@@ -26,16 +24,19 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
|
||||
icon: <CheckOutlined/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip title={rdapStatusCodeDetailTranslated["auto renew period"]}>{t`Auto-Renew Grace Period`}</Tooltip>,
|
||||
title: <Tooltip
|
||||
title={rdapStatusCodeDetailTranslated['auto renew period']}>{t`Auto-Renew Grace Period`}</Tooltip>,
|
||||
icon: <FieldTimeOutlined style={{color: 'palevioletred'}}/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip
|
||||
title={rdapStatusCodeDetailTranslated["redemption period"]}>{t`Redemption Grace Period`}</Tooltip>,
|
||||
title={rdapStatusCodeDetailTranslated['redemption period']}
|
||||
>{t`Redemption Grace Period`}
|
||||
</Tooltip>,
|
||||
icon: <ExclamationCircleOutlined style={{color: 'magenta'}}/>
|
||||
},
|
||||
{
|
||||
title: <Tooltip title={rdapStatusCodeDetailTranslated["pending delete"]}>{t`Pending Delete`}</Tooltip>,
|
||||
title: <Tooltip title={rdapStatusCodeDetailTranslated['pending delete']}>{t`Pending Delete`}</Tooltip>,
|
||||
icon: <DeleteOutlined style={{color: 'orangered'}}/>
|
||||
}
|
||||
]
|
||||
@@ -50,8 +51,10 @@ export function DomainLifecycleSteps({status}: { status: string[] }) {
|
||||
currentStep = 4
|
||||
}
|
||||
|
||||
return <Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,107 +1,121 @@
|
||||
import {Badge, Card, Col, Divider, Flex, Row, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {EventTimeline} from "./EventTimeline";
|
||||
import {EntitiesList} from "./EntitiesList";
|
||||
import {DomainDiagram} from "./DomainDiagram";
|
||||
import React from "react";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {regionNames} from "../../i18n";
|
||||
import {Badge, Card, Col, Divider, Flex, Row, Space, Tag, Tooltip, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {EventTimeline} from './EventTimeline'
|
||||
import {EntitiesList} from './EntitiesList'
|
||||
import {DomainDiagram} from './DomainDiagram'
|
||||
import React from 'react'
|
||||
import {Domain} from '../../utils/api'
|
||||
import {regionNames} from '../../i18n'
|
||||
|
||||
import {getCountryCode} from "../../utils/functions/getCountryCode";
|
||||
import {eppStatusCodeToColor} from "../../utils/functions/eppStatusCodeToColor";
|
||||
import {DomainLifecycleSteps} from "./DomainLifecycleSteps";
|
||||
import {getCountryCode} from '../../utils/functions/getCountryCode'
|
||||
import {DomainLifecycleSteps} from './DomainLifecycleSteps'
|
||||
import {BankOutlined, KeyOutlined, SafetyCertificateOutlined} from '@ant-design/icons'
|
||||
import {statusToTag} from '../tracking/StatusToTag'
|
||||
|
||||
export function DomainResult({domain}: { domain: Domain }) {
|
||||
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
const {tld, events} = domain
|
||||
const domainEvents = events.sort((e1, e2) => new Date(e2.date).getTime() - new Date(e1.date).getTime())
|
||||
const clientStatus = domain.status.filter(s => s.startsWith('client'))
|
||||
const serverStatus = domain.status.filter(s => !clientStatus.includes(s))
|
||||
|
||||
const isLocked = (type: 'client' | 'server'): boolean =>
|
||||
(domain.status.includes(type + ' update prohibited') && domain.status.includes(type + ' delete prohibited'))
|
||||
|| domain.status.includes(type + ' transfer prohibited')
|
||||
const isDomainLocked = (type: 'client' | 'server'): boolean =>
|
||||
(domain.status.includes(type + ' update prohibited') && domain.status.includes(type + ' delete prohibited')) ||
|
||||
domain.status.includes(type + ' transfer prohibited')
|
||||
|
||||
const statusToTag = (s: string) => <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
return (
|
||||
<Space direction='vertical' size='middle' style={{width: '100%'}}>
|
||||
|
||||
return <Space direction="vertical" size="middle" style={{width: '100%'}}>
|
||||
|
||||
<Badge.Ribbon text={
|
||||
<Tooltip
|
||||
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}>
|
||||
{`${domain.tld.tld.toUpperCase()} (${tld.type})`}
|
||||
</Tooltip>
|
||||
}
|
||||
color={
|
||||
tld.type === 'ccTLD' ? 'purple' :
|
||||
(tld.type === 'gTLD' && tld.specification13) ? "volcano" :
|
||||
tld.type === 'gTLD' ? "green"
|
||||
: "cyan"
|
||||
}>
|
||||
|
||||
<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}/>
|
||||
<Badge.Ribbon
|
||||
text={
|
||||
<Tooltip
|
||||
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}
|
||||
>
|
||||
{`${domain.tld.tld.toUpperCase()} (${tld.type})`}
|
||||
</Tooltip>
|
||||
}
|
||||
<Row gutter={8}>
|
||||
<Col span={24} xl={12} xxl={12}>
|
||||
<Flex justify='center' align='center' style={{margin: 10}} wrap gap="4px 0">
|
||||
<Tooltip
|
||||
title={t`Registry-level protection, ensuring the highest level of security by preventing unauthorized, unwanted, or accidental changes to the domain name at the registry level`}>
|
||||
<Tag bordered={false} color={isLocked('server') ? 'green' : 'default'}
|
||||
icon={<SafetyCertificateOutlined
|
||||
style={{fontSize: '16px'}}/>}>{t`Registry Lock`}</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t`Registrar-level protection, safeguarding the domain from unauthorized, unwanted, or accidental changes through registrar controls`}>
|
||||
<Tag bordered={false} color={isLocked('client') ? 'green' : 'default'}
|
||||
icon={<BankOutlined
|
||||
style={{fontSize: '16px'}}/>}>{t`Registrar Lock`}</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t`DNSSEC secures DNS by adding cryptographic signatures to DNS records, ensuring authenticity and integrity of responses`}>
|
||||
<Tag bordered={false} color={domain.delegationSigned ? 'green' : 'default'}
|
||||
icon={<KeyOutlined style={{fontSize: '16px'}}/>}>{t`DNSSEC`}</Tag>
|
||||
</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>
|
||||
}
|
||||
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}>
|
||||
<Col span={24} xl={12} xxl={12}>
|
||||
<Flex justify='center' align='center' style={{margin: 10}} wrap gap='4px 0'>
|
||||
<Tooltip
|
||||
title={t`Registry-level protection, ensuring the highest level of security by preventing unauthorized, unwanted, or accidental changes to the domain name at the registry level`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false} color={isDomainLocked('server') ? 'green' : 'default'}
|
||||
icon={<SafetyCertificateOutlined
|
||||
style={{fontSize: '16px'}}
|
||||
/>}
|
||||
>{t`Registry Lock`}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t`Registrar-level protection, safeguarding the domain from unauthorized, unwanted, or accidental changes through registrar controls`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false} color={isDomainLocked('client') ? 'green' : 'default'}
|
||||
icon={<BankOutlined
|
||||
style={{fontSize: '16px'}}
|
||||
/>}
|
||||
>{t`Registrar Lock`}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={t`DNSSEC secures DNS by adding cryptographic signatures to DNS records, ensuring authenticity and integrity of responses`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false} color={domain.delegationSigned ? 'green' : 'default'}
|
||||
icon={<KeyOutlined style={{fontSize: '16px'}}/>}
|
||||
>{t`DNSSEC`}
|
||||
</Tag>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import {Form, Input} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Form, Input} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {SearchOutlined} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
export type FieldType = {
|
||||
export interface FieldType {
|
||||
ldhName: string
|
||||
}
|
||||
|
||||
export function DomainSearchBar({onFinish, initialValue}: { onFinish: (values: FieldType) => void, initialValue?: string }) {
|
||||
return <Form
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
style={{width: '100%'}}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
name="ldhName"
|
||||
initialValue={initialValue}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /^(?=.*\.)?\S*[^.\s]$/,
|
||||
message: t`This domain name does not appear to be valid`,
|
||||
max: 63,
|
||||
min: 2
|
||||
}]}
|
||||
export function DomainSearchBar({onFinish, initialValue}: {
|
||||
onFinish: (values: FieldType) => void,
|
||||
initialValue?: string
|
||||
}) {
|
||||
return (
|
||||
<Form
|
||||
onFinish={onFinish}
|
||||
autoComplete='off'
|
||||
style={{width: '100%'}}
|
||||
>
|
||||
<Input style={{textAlign: 'center'}}
|
||||
size="large"
|
||||
prefix={<SearchOutlined/>}
|
||||
placeholder="example.com"
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
<Form.Item<FieldType>
|
||||
name='ldhName'
|
||||
initialValue={initialValue}
|
||||
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'}}
|
||||
size='large'
|
||||
prefix={<SearchOutlined/>}
|
||||
placeholder='example.com'
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import {List, Tag, Tooltip, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Domain} from "../../utils/api";
|
||||
import {rdapRoleDetailTranslation, rdapRoleTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {roleToAvatar} from "../../utils/functions/roleToAvatar";
|
||||
import {rolesToColor} from "../../utils/functions/rolesToColor";
|
||||
import {sortDomainEntities} from "../../utils/functions/sortDomainEntities";
|
||||
import {extractDetailsFromJCard} from "../../utils/functions/extractDetailsFromJCard";
|
||||
import {List, Tag, Tooltip, Typography} from 'antd'
|
||||
import React from 'react'
|
||||
import {Domain} from '../../utils/api'
|
||||
import {rdapRoleDetailTranslation, rdapRoleTranslation} from '../../utils/functions/rdapTranslation'
|
||||
import {roleToAvatar} from '../../utils/functions/roleToAvatar'
|
||||
import {rolesToColor} from '../../utils/functions/rolesToColor'
|
||||
import {sortDomainEntities} from '../../utils/functions/sortDomainEntities'
|
||||
import {extractDetailsFromJCard} from '../../utils/functions/extractDetailsFromJCard'
|
||||
|
||||
export function EntitiesList({domain}: { domain: Domain }) {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
const rdapRoleDetailTranslated = rdapRoleDetailTranslation()
|
||||
|
||||
const roleToTag = (r: string) => <Tooltip
|
||||
title={rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] || undefined}>
|
||||
<Tag color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r
|
||||
}</Tag>
|
||||
title={rdapRoleDetailTranslated[r as keyof typeof rdapRoleDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={rolesToColor([r])}>{rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
|
||||
return <List
|
||||
className="demo-loadmore-list"
|
||||
itemLayout="horizontal"
|
||||
dataSource={sortDomainEntities(domain)}
|
||||
renderItem={(e) => {
|
||||
const details = extractDetailsFromJCard(e)
|
||||
return (
|
||||
<List
|
||||
className='demo-loadmore-list'
|
||||
itemLayout='horizontal'
|
||||
dataSource={sortDomainEntities(domain)}
|
||||
renderItem={(e) => {
|
||||
const details = extractDetailsFromJCard(e)
|
||||
|
||||
return <List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={roleToAvatar(e)}
|
||||
title={<Typography.Text code>{e.entity.handle}</Typography.Text>}
|
||||
description={<>
|
||||
{details.fn && <div>👤 {details.fn}</div>}
|
||||
{details.organization && <div>🏢 {details.organization}</div>}
|
||||
</>}
|
||||
/>
|
||||
{e.roles.map(roleToTag)}
|
||||
</List.Item>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={roleToAvatar(e)}
|
||||
title={<Typography.Text code>{e.entity.handle}</Typography.Text>}
|
||||
description={<>
|
||||
{details.fn && <div>👤 {details.fn}</div>}
|
||||
{details.organization && <div>🏢 {details.organization}</div>}
|
||||
</>}
|
||||
/>
|
||||
{e.roles.map(roleToTag)}
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Timeline, Tooltip, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Event} from "../../utils/api";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../utils/functions/rdapTranslation";
|
||||
import {actionToColor} from "../../utils/functions/actionToColor";
|
||||
import {actionToIcon} from "../../utils/functions/actionToIcon";
|
||||
import {Timeline, Tooltip, Typography} from 'antd'
|
||||
import React from 'react'
|
||||
import {Event} from '../../utils/api'
|
||||
import useBreakpoint from '../../hooks/useBreakpoint'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../utils/functions/rdapTranslation'
|
||||
import {actionToColor} from '../../utils/functions/actionToColor'
|
||||
import {actionToIcon} from '../../utils/functions/actionToIcon'
|
||||
|
||||
export function EventTimeline({events}: { events: Event[] }) {
|
||||
const sm = useBreakpoint('sm')
|
||||
@@ -13,38 +13,46 @@ export function EventTimeline({events}: { events: Event[] }) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
return <>
|
||||
<Timeline
|
||||
mode={sm ? "left" : "right"}
|
||||
items={events.map(e => {
|
||||
const eventName = <Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
|
||||
{rdapEventNameTranslated[e.action as keyof typeof rdapEventNameTranslated] || e.action}
|
||||
</Typography.Text>
|
||||
return (
|
||||
<>
|
||||
<Timeline
|
||||
mode={sm ? 'left' : 'right'}
|
||||
items={events.map(e => {
|
||||
const eventName = (
|
||||
<Typography.Text style={{color: e.deleted ? 'grey' : 'default'}}>
|
||||
{rdapEventNameTranslated[e.action as keyof typeof rdapEventNameTranslated] || e.action}
|
||||
</Typography.Text>
|
||||
)
|
||||
|
||||
const dateStr = <Typography.Text
|
||||
style={{color: e.deleted ? 'grey' : 'default'}}>{new Date(e.date).toLocaleString(locale)}
|
||||
</Typography.Text>
|
||||
const dateStr = (
|
||||
<Typography.Text
|
||||
style={{color: e.deleted ? 'grey' : 'default'}}
|
||||
>{new Date(e.date).toLocaleString(locale)}
|
||||
</Typography.Text>
|
||||
)
|
||||
|
||||
const eventDetail = rdapEventDetailTranslated[e.action as keyof typeof rdapEventDetailTranslated] || undefined
|
||||
const eventDetail = rdapEventDetailTranslated[e.action as keyof typeof rdapEventDetailTranslated] || undefined
|
||||
|
||||
const text = sm ? {
|
||||
children: <Tooltip placement='bottom' title={eventDetail}>
|
||||
{eventName} {dateStr}
|
||||
</Tooltip>
|
||||
} : {
|
||||
label: dateStr,
|
||||
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>,
|
||||
const text = sm
|
||||
? {
|
||||
children: <Tooltip placement='bottom' title={eventDetail}>
|
||||
{eventName} {dateStr}
|
||||
</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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import {Tag} from "antd";
|
||||
import {DeleteOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import punycode from "punycode/punycode";
|
||||
import {Link} from "react-router-dom";
|
||||
import React from "react";
|
||||
import {Tag} from 'antd'
|
||||
import {DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
|
||||
import punycode from 'punycode/punycode'
|
||||
import {Link} from 'react-router-dom'
|
||||
import React from 'react'
|
||||
|
||||
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[] } }) {
|
||||
return <Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted ? 'magenta' :
|
||||
domain.status.includes('redemption period') ? 'yellow' :
|
||||
domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted ? <DeleteOutlined/> :
|
||||
domain.status.includes('redemption period') ? <ExclamationCircleOutlined/> :
|
||||
domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}>{punycode.toUnicode(domain.ldhName)}</Tag>
|
||||
</Link>
|
||||
}
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
17
assets/components/tracking/StatusToTag.tsx
Normal file
17
assets/components/tracking/StatusToTag.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
|
||||
import {t} from "ttag";
|
||||
import {BankOutlined} from "@ant-design/icons";
|
||||
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from 'antd'
|
||||
import React, {useState} from 'react'
|
||||
import {Connector, ConnectorProvider} from '../../../utils/api/connectors'
|
||||
import {t} from 'ttag'
|
||||
import {BankOutlined} from '@ant-design/icons'
|
||||
import {
|
||||
ovhEndpointList as ovhEndpointListFunction,
|
||||
ovhFields as ovhFieldsFunction,
|
||||
ovhPricingMode as ovhPricingModeFunction,
|
||||
ovhSubsidiaryList as ovhSubsidiaryListFunction
|
||||
} from "../../../utils/providers/ovh";
|
||||
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
|
||||
} from '../../../utils/providers/ovh'
|
||||
import {helpGetTokenLink, tosHyperlink} from '../../../utils/providers'
|
||||
|
||||
const formItemLayoutWithOutLabel = {
|
||||
wrapperCol: {
|
||||
xs: {span: 24, offset: 0},
|
||||
sm: {span: 20, offset: 4},
|
||||
},
|
||||
sm: {span: 20, offset: 4}
|
||||
}
|
||||
}
|
||||
|
||||
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
|
||||
@@ -27,229 +27,257 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
|
||||
const [open, setOpen] = useState(false)
|
||||
const [ovhPricingModeValue, setOvhPricingModeValue] = useState<string | undefined>()
|
||||
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 14}}
|
||||
onFinish={onCreate}
|
||||
>
|
||||
<Form.Item
|
||||
label={t`Provider`}
|
||||
name="provider"
|
||||
help={helpGetTokenLink(provider)}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
layout='horizontal'
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 14}}
|
||||
onFinish={onCreate}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t`Please select a Provider`}
|
||||
suffixIcon={<BankOutlined/>}
|
||||
options={Object.keys(ConnectorProvider).map((c) => ({
|
||||
value: ConnectorProvider[c as keyof typeof ConnectorProvider],
|
||||
label: (
|
||||
<>
|
||||
<BankOutlined/>{" "} {c}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Provider`}
|
||||
name='provider'
|
||||
help={helpGetTokenLink(provider)}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t`Please select a Provider`}
|
||||
suffixIcon={<BankOutlined/>}
|
||||
options={Object.keys(ConnectorProvider).map((c) => ({
|
||||
value: ConnectorProvider[c as keyof typeof ConnectorProvider],
|
||||
label: (
|
||||
<>
|
||||
<BankOutlined/>{' '} {c}
|
||||
</>
|
||||
)
|
||||
}))}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{
|
||||
provider === ConnectorProvider.OVH && <>
|
||||
{
|
||||
Object.keys(ovhFields).map(fieldName => <Form.Item
|
||||
label={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
name={['authData', fieldName]}
|
||||
{
|
||||
provider === ConnectorProvider.OVH && <>
|
||||
{
|
||||
Object.keys(ovhFields).map(fieldName => <Form.Item
|
||||
key={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
label={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
name={['authData', fieldName]}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>)
|
||||
}
|
||||
<Form.Item
|
||||
label={t`OVH Endpoint`}
|
||||
name={['authData', 'apiEndpoint']}
|
||||
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`}]}
|
||||
>
|
||||
<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={ovhEndpointList} optionFilterProp='label'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`OVH subsidiary`}
|
||||
name={['authData', 'ovhSubsidiary']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<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) => {
|
||||
setOvhPricingModeValue(value)
|
||||
form.setFieldValue(['authData', 'pricingMode'], value)
|
||||
if (value !== 'create-default') {
|
||||
setOpen(true)
|
||||
}
|
||||
}}/>
|
||||
</Popconfirm>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.GANDI && <>
|
||||
<Form.Item
|
||||
label={t`Personal Access Token (PAT)`}
|
||||
name={['authData', 'token']}
|
||||
rules={[{required: true, message: t`Required`}]}>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Organization sharing ID`}
|
||||
name={['authData', 'sharingId']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`It indicates the organization that will pay for the ordered product`}</Typography.Text>}
|
||||
required={false}>
|
||||
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.AUTODNS && <>
|
||||
<Alert
|
||||
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
|
||||
type="warning"/>
|
||||
<br/>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Username`}
|
||||
name={['authData', 'username']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}>
|
||||
<Input autoComplete='off' required={true}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Password`}
|
||||
name={['authData', 'password']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required={true}>
|
||||
<Input.Password autoComplete='off' required={true} placeholder=''/>
|
||||
</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={true}>
|
||||
<Input autoComplete='off' required={true} placeholder=''/>
|
||||
</Form.Item>
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.GANDI && <>
|
||||
<Form.Item
|
||||
label={t`Personal Access Token (PAT)`}
|
||||
name={['authData', 'token']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Organization sharing ID`}
|
||||
name={['authData', 'sharingId']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`It indicates the organization that will pay for the ordered product`}
|
||||
</Typography.Text>}
|
||||
required={false}
|
||||
>
|
||||
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.AUTODNS && <>
|
||||
<Alert
|
||||
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
|
||||
type='warning'
|
||||
/>
|
||||
<br/>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Username`}
|
||||
name={['authData', 'username']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}
|
||||
</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off' required/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Password`}
|
||||
name={['authData', 'password']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required
|
||||
>
|
||||
<Input.Password autoComplete='off' required placeholder=''/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Owner nic-handle`}
|
||||
name={['authData', 'contactid']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`The nic-handle of the domain name owner`}<a
|
||||
href='https://cloud.autodns.com/contacts/domain'
|
||||
>{t`You can get it from this page`}
|
||||
</a>
|
||||
</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required
|
||||
>
|
||||
<Input autoComplete='off' required placeholder=''/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`Context Value`}
|
||||
name={['authData', 'context']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`If you not sure, use the default value 4`}</Typography.Text>}
|
||||
<Form.Item
|
||||
label={t`Context Value`}
|
||||
name={['authData', 'context']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`If you not sure, use the default value 4`}
|
||||
</Typography.Text>}
|
||||
|
||||
required={false}>
|
||||
<Input autoComplete='off' required={false} placeholder='4'/>
|
||||
</Form.Item>
|
||||
required={false}
|
||||
>
|
||||
<Input autoComplete='off' required={false} placeholder='4'/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Owner confirmation`}
|
||||
name={['authData', 'ownerConfirm']}
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Owner confirmation`}
|
||||
name={['authData', 'ownerConfirm']}
|
||||
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`Owner confirms his consent of domain order jobs`}</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`Owner confirms his consent of domain order jobs`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.NAMECHEAP && <>
|
||||
<Form.Item
|
||||
label={t`Username`}
|
||||
name={['authData', 'ApiUser']}
|
||||
>
|
||||
<Input autoComplete='off'></Input>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`API key`}
|
||||
name={['authData', 'ApiKey']}
|
||||
>
|
||||
<Input autoComplete='off'></Input>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.NAMECHEAP && <>
|
||||
<Form.Item
|
||||
label={t`Username`}
|
||||
name={['authData', 'ApiUser']}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`API key`}
|
||||
name={['authData', 'ApiKey']}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
provider !== undefined && <>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`API Terms of Service`}
|
||||
name={['authData', 'acceptConditions']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
style={{marginTop: '3em'}}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>
|
||||
<Typography.Link target='_blank' href={tosHyperlink(provider)}>
|
||||
{t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
|
||||
</Typography.Link>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`Legal age`}
|
||||
name={['authData', 'ownerLegalAge']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`I am of the minimum age required to consent to these conditions`}</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`Withdrawal period`}
|
||||
name={['authData', 'waiveRetractationPeriod']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider !== undefined && <>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`API Terms of Service`}
|
||||
name={['authData', 'acceptConditions']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
style={{marginTop: '3em'}}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>
|
||||
<Typography.Link target='_blank' href={tosHyperlink(provider)}>
|
||||
{t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
|
||||
</Typography.Link>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Legal age`}
|
||||
name={['authData', 'ownerLegalAge']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`I am of the minimum age required to consent to these conditions`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Withdrawal period`}
|
||||
name={['authData', 'waiveRetractationPeriod']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t`Create`}
|
||||
</Button>
|
||||
<Button type='default' htmlType='reset'>
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector, deleteConnector} from "../../../utils/api/connectors";
|
||||
|
||||
const {useToken} = theme;
|
||||
import {Card, Divider, message, Popconfirm, theme, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {DeleteFilled} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import {Connector, deleteConnector} from '../../../utils/api/connectors'
|
||||
|
||||
const {useToken} = theme
|
||||
|
||||
export type ConnectorElement = Connector & { id: string, createdAt: string }
|
||||
|
||||
@@ -13,28 +12,36 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
|
||||
const {token} = useToken()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
|
||||
const onConnectorDelete = async (connector: ConnectorElement) => await deleteConnector(connector.id)
|
||||
.then(onDelete)
|
||||
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
|
||||
|
||||
return <>
|
||||
{connectors.map(connector =>
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card hoverable title={<Typography.Text
|
||||
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Popconfirm title={t`Delete the Connector`}
|
||||
description={t`Are you sure to delete this Connector?`}
|
||||
onConfirm={() => onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>
|
||||
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{connectors.map(connector =>
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card
|
||||
hoverable title={<Typography.Text
|
||||
title={new Date(connector.createdAt).toLocaleString()}
|
||||
>{t`Connector ${connector.provider}`}
|
||||
</Typography.Text>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Popconfirm
|
||||
title={t`Delete the Connector`}
|
||||
description={t`Are you sure to delete this Connector?`}
|
||||
onConfirm={async () => await onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/>
|
||||
</Popconfirm>}
|
||||
>
|
||||
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import {CalendarFilled} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {Popover, QRCode, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {CalendarFilled} from '@ant-design/icons'
|
||||
import {t} from 'ttag'
|
||||
import {Popover, QRCode, Typography} from 'antd'
|
||||
import React from 'react'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
|
||||
|
||||
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
|
||||
|
||||
return <Typography.Link href={icsResourceLink}>
|
||||
<Popover content={<QRCode value={icsResourceLink}
|
||||
bordered={false}
|
||||
title={t`QR Code for iCalendar export`}
|
||||
type='svg'
|
||||
/>}>
|
||||
<CalendarFilled title={t`Export events to iCalendar format`}
|
||||
style={{color: 'limegreen'}}
|
||||
/>
|
||||
</Popover>
|
||||
</Typography.Link>
|
||||
}
|
||||
return (
|
||||
<Typography.Link href={icsResourceLink}>
|
||||
<Popover content={<QRCode
|
||||
value={icsResourceLink}
|
||||
bordered={false}
|
||||
title={t`QR Code for iCalendar export`}
|
||||
type='svg'
|
||||
/>}
|
||||
>
|
||||
<CalendarFilled
|
||||
title={t`Export events to iCalendar format`}
|
||||
style={{color: 'limegreen'}}
|
||||
/>
|
||||
</Popover>
|
||||
</Typography.Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import {Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {deleteWatchlist} from "../../../utils/api";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Popconfirm, theme, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {deleteWatchlist, Watchlist} from '../../../utils/api'
|
||||
import {DeleteFilled} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
|
||||
const {token} = theme.useToken()
|
||||
|
||||
return <Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}>
|
||||
<Typography.Link>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm>
|
||||
}
|
||||
return (
|
||||
<Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={async () => await deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}
|
||||
>
|
||||
<Typography.Link>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import {Domain, getTrackedDomainList} from "../../../utils/api";
|
||||
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
|
||||
import {Link} from "react-router-dom";
|
||||
import React, {ReactElement, useEffect, useState} from 'react'
|
||||
import {Domain, getTrackedDomainList} from '../../../utils/api'
|
||||
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {ColumnType} from 'antd/es/table'
|
||||
import {rdapStatusCodeDetailTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {eppStatusCodeToColor} from '../../../utils/functions/eppStatusCodeToColor'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons'
|
||||
import {DomainToTag} from "../DomainToTag";
|
||||
|
||||
import {DomainToTag} from '../DomainToTag'
|
||||
|
||||
export function TrackedDomainTable() {
|
||||
const REDEMPTION_NOTICE = <Tooltip
|
||||
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}>
|
||||
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
|
||||
</Tooltip>
|
||||
const REDEMPTION_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const PENDING_DELETE_NOTICE = <Tooltip
|
||||
title={t`At least one domain name is pending deletion and will soon become available for registration again`}>
|
||||
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
|
||||
</Tooltip>
|
||||
const PENDING_DELETE_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is pending deletion and will soon become available for registration again`}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const [dataTable, setDataTable] = useState<(Domain & { domain: Domain })[]>([])
|
||||
interface TableRow {
|
||||
key: string
|
||||
ldhName: ReactElement
|
||||
expirationDate: string
|
||||
status: ReactElement[]
|
||||
updatedAt: string
|
||||
domain: Domain
|
||||
}
|
||||
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState<number>()
|
||||
const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([])
|
||||
|
||||
@@ -46,8 +60,10 @@ export function TrackedDomainTable() {
|
||||
ldhName: <DomainToTag domain={d}/>,
|
||||
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
|
||||
status: d.status.map(s => <Tooltip
|
||||
key={s}
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -63,17 +79,19 @@ export function TrackedDomainTable() {
|
||||
fetchData({page: 1, itemsPerPage: 30})
|
||||
}, [])
|
||||
|
||||
interface RecordType {
|
||||
domain: Domain
|
||||
}
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
const columns: Array<ColumnType<RecordType>> = [
|
||||
{
|
||||
title: t`Domain`,
|
||||
dataIndex: "ldhName"
|
||||
dataIndex: 'ldhName'
|
||||
},
|
||||
{
|
||||
title: t`Expiration date`,
|
||||
dataIndex: 'expirationDate',
|
||||
sorter: (a: { domain: Domain }, b: { domain: Domain }) => {
|
||||
|
||||
sorter: (a: RecordType, b: RecordType) => {
|
||||
const expirationDate1 = a.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
const expirationDate2 = b.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
|
||||
@@ -85,65 +103,70 @@ export function TrackedDomainTable() {
|
||||
{
|
||||
title: t`Updated at`,
|
||||
dataIndex: 'updatedAt',
|
||||
sorter: (a: { domain: Domain }, b: {
|
||||
domain: Domain
|
||||
}) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
|
||||
sorter: (a: RecordType, b: RecordType) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
|
||||
},
|
||||
{
|
||||
title: t`Status`,
|
||||
dataIndex: 'status',
|
||||
showSorterTooltip: {target: 'full-header'},
|
||||
filters: [...new Set(dataTable.map((d: any) => d.domain.status).flat())].map(s => ({
|
||||
filters: [...new Set(dataTable.map((d: RecordType) => d.domain.status).flat())].map(s => ({
|
||||
text: <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>,
|
||||
value: s,
|
||||
value: s
|
||||
})),
|
||||
onFilter: (value, record: { domain: Domain }) => record.domain.status.includes(value as string)
|
||||
onFilter: (value, record: RecordType) => record.domain.status.includes(value as string)
|
||||
}
|
||||
]
|
||||
|
||||
return <>
|
||||
{
|
||||
total === 0 ? <Empty
|
||||
description={t`No tracked domain names were found, please create your first Watchlist`}
|
||||
>
|
||||
<Link to='/tracking/watchlist'>
|
||||
<Button type="primary">Create Now</Button>
|
||||
</Link>
|
||||
</Empty> : <Skeleton loading={total === undefined}>
|
||||
<Result
|
||||
style={{paddingTop: 0}}
|
||||
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
|
||||
{...(specialNotice.length > 0 ? {
|
||||
icon: <ExceptionOutlined/>,
|
||||
status: 'warning',
|
||||
title: t`At least one domain name you are tracking requires special attention`,
|
||||
extra: specialNotice
|
||||
} : {
|
||||
icon: <MonitorOutlined/>,
|
||||
status: 'info',
|
||||
title: t`The domain names below are subject to special monitoring`,
|
||||
})}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
{
|
||||
total === 0
|
||||
? <Empty
|
||||
description={t`No tracked domain names were found, please create your first Watchlist`}
|
||||
>
|
||||
<Link to='/tracking/watchlist'>
|
||||
<Button type='primary'>Create Now</Button>
|
||||
</Link>
|
||||
</Empty>
|
||||
: <Skeleton loading={total === undefined}>
|
||||
<Result
|
||||
style={{paddingTop: 0}}
|
||||
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
|
||||
{...(specialNotice.length > 0
|
||||
? {
|
||||
icon: <ExceptionOutlined/>,
|
||||
status: 'warning',
|
||||
title: t`At least one domain name you are tracking requires special attention`,
|
||||
extra: specialNotice
|
||||
}
|
||||
: {
|
||||
icon: <MonitorOutlined/>,
|
||||
status: 'info',
|
||||
title: t`The domain names below are subject to special monitoring`
|
||||
})}
|
||||
/>
|
||||
|
||||
<Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
scroll={{y: '50vh'}}
|
||||
/>
|
||||
</Skeleton>
|
||||
}
|
||||
</>
|
||||
}
|
||||
<Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
scroll={{y: '50vh'}}
|
||||
/>
|
||||
</Skeleton>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import {Button, Drawer, Form, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {WatchlistForm} from "./WatchlistForm";
|
||||
import React, {useState} from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {Button, Drawer, Form, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from './WatchlistForm'
|
||||
import React, {useState} from 'react'
|
||||
import {EditOutlined} from '@ant-design/icons'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
|
||||
const showDrawer = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
@@ -26,43 +24,46 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<EditOutlined title={t`Edit the Watchlist`} onClick={() => {
|
||||
showDrawer()
|
||||
form.setFields([
|
||||
{name: 'token', value: watchlist.token},
|
||||
{name: 'name', value: watchlist.name},
|
||||
{name: 'connector', value: watchlist.connector?.id},
|
||||
{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))
|
||||
return (
|
||||
<>
|
||||
<Typography.Link>
|
||||
<EditOutlined
|
||||
title={t`Edit the Watchlist`} onClick={() => {
|
||||
showDrawer()
|
||||
form.setFields([
|
||||
{name: 'token', value: watchlist.token},
|
||||
{name: 'name', value: watchlist.name},
|
||||
{name: 'connector', value: watchlist.connector?.id},
|
||||
{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}
|
||||
])
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,83 @@
|
||||
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from "antd";
|
||||
import {DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
|
||||
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
|
||||
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint";
|
||||
import {CalendarWatchlistButton} from "./CalendarWatchlistButton";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from 'antd'
|
||||
import {DisconnectOutlined, LinkOutlined} from '@ant-design/icons'
|
||||
import {t} from 'ttag'
|
||||
import {ViewDiagramWatchlistButton} from './diagram/ViewDiagramWatchlistButton'
|
||||
import {UpdateWatchlistButton} from './UpdateWatchlistButton'
|
||||
import {DeleteWatchlistButton} from './DeleteWatchlistButton'
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {CalendarWatchlistButton} from './CalendarWatchlistButton'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
import {DomainToTag} from "../DomainToTag";
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {DomainToTag} from '../DomainToTag'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[],
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
return <>
|
||||
<Card
|
||||
type='inner'
|
||||
title={<>
|
||||
{
|
||||
watchlist.connector ?
|
||||
<Tooltip title={watchlist.connector.id}>
|
||||
<Tag icon={<LinkOutlined/>} color="lime-inverse"/>
|
||||
</Tooltip> :
|
||||
<Tooltip title={t`This Watchlist is not linked to a Connector.`}>
|
||||
<Tag icon={<DisconnectOutlined/>} color="default"/>
|
||||
</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>
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
type='inner'
|
||||
title={<>
|
||||
{
|
||||
(watchlist.connector != null)
|
||||
? <Tooltip title={watchlist.connector.id}>
|
||||
<Tag icon={<LinkOutlined/>} color='lime-inverse'/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
}
|
||||
: <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 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/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,55 @@
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
import {actionToIcon} from "../../../utils/functions/actionToIcon";
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {actionToIcon} from '../../../utils/functions/actionToIcon'
|
||||
import {EventAction} from '../../../utils/api'
|
||||
|
||||
type TagRender = SelectProps['tagRender'];
|
||||
type TagRender = SelectProps['tagRender']
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
sm: {span: 4}
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 20},
|
||||
},
|
||||
};
|
||||
sm: {span: 20}
|
||||
}
|
||||
}
|
||||
|
||||
const formItemLayoutWithOutLabel = {
|
||||
wrapperCol: {
|
||||
xs: {span: 24, offset: 0},
|
||||
sm: {span: 20, offset: 4},
|
||||
},
|
||||
};
|
||||
sm: {span: 20, offset: 4}
|
||||
}
|
||||
}
|
||||
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
form: FormInstance,
|
||||
connectors: (Connector & { id: string })[]
|
||||
form: FormInstance
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
|
||||
isCreation: boolean
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const triggerTagRenderer: TagRender = (props) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const triggerTagRenderer: TagRender = ({value, closable, onClose}: {
|
||||
value: EventAction
|
||||
closable: boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return (<Tooltip
|
||||
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}>
|
||||
return (
|
||||
<Tooltip
|
||||
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag
|
||||
icon={actionToIcon(value)}
|
||||
color={actionToColor(value)}
|
||||
@@ -58,203 +64,220 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
)
|
||||
}
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
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},
|
||||
}}
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
>
|
||||
<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 Promise.reject(new Error(t`At least one domain name`));
|
||||
|
||||
<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.`}
|
||||
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.map((field, index) => (
|
||||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`Domain names` : ''}
|
||||
required={true}
|
||||
key={field.key}
|
||||
>
|
||||
}
|
||||
]}
|
||||
>
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<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
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`Domain names` : ''}
|
||||
required
|
||||
key={field.key}
|
||||
>
|
||||
<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>
|
||||
{fields.length > 1 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type='dashed'
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Domain name`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Domain name`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item label={t`Tracked events`}
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20},
|
||||
}}
|
||||
required
|
||||
>
|
||||
<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.List>
|
||||
<Form.Item
|
||||
label={t`Tracked events`}
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<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`}
|
||||
name='connector'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
}}
|
||||
wrapperCol={{
|
||||
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.`}
|
||||
>
|
||||
<Select showSearch
|
||||
<Form.Item
|
||||
label={t`Connector`}
|
||||
name='connector'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
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.`}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={t`Connector`}
|
||||
suffixIcon={<ApiOutlined/>}
|
||||
optionFilterProp="label"
|
||||
optionFilterProp='label'
|
||||
options={connectors.map(c => ({
|
||||
label: `${c.provider} (${c.id})`,
|
||||
value: c.id
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name="dsn">
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`DSN` : ''}
|
||||
required={true}
|
||||
key={field.key}
|
||||
>
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name='dsn'
|
||||
>
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /:\/\//,
|
||||
message: t`This DSN does not appear to be valid`
|
||||
}]}
|
||||
noStyle
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`DSN` : ''}
|
||||
required
|
||||
key={field.key}
|
||||
>
|
||||
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
|
||||
autoComplete='off'/>
|
||||
<Form.Item
|
||||
{...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>
|
||||
{fields.length > 0 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
))}
|
||||
<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/>}
|
||||
))}
|
||||
<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>
|
||||
}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
<Button
|
||||
type='dashed'
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type='default' htmlType='reset'>
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {WatchlistCard} from "./WatchlistCard";
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {WatchlistCard} from './WatchlistCard'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
|
||||
watchlists: Watchlist[],
|
||||
onDelete: () => void,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
watchlists: Watchlist[]
|
||||
onDelete: () => void
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard
|
||||
key={watchlist.token}
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {Button, Flex, Modal, Space, Typography} from "antd"
|
||||
import {t} from "ttag"
|
||||
import React, {useEffect, useState} from "react"
|
||||
import {ApartmentOutlined} from "@ant-design/icons"
|
||||
import {Button, Flex, Modal, Space, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {ApartmentOutlined} from '@ant-design/icons'
|
||||
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
|
||||
import {getWatchlist} from "../../../../utils/api";
|
||||
import {getLayoutedElements} from "./getLayoutedElements";
|
||||
import {watchlistToNodes} from "./watchlistToNodes";
|
||||
import {watchlistToEdges} from "./watchlistToEdges";
|
||||
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
|
||||
import {getWatchlist} from '../../../../utils/api'
|
||||
import {getLayoutedElements} from './getLayoutedElements'
|
||||
import {watchlistToNodes} from './watchlistToNodes'
|
||||
import {watchlistToEdges} from './watchlistToEdges'
|
||||
|
||||
export function ViewDiagramWatchlistButton({token}: { token: string }) {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
useEffect(() => {
|
||||
setEdges([])
|
||||
@@ -30,52 +29,54 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
|
||||
setNodes(e.nodes)
|
||||
setEdges(e.edges)
|
||||
}).catch(() => setOpen(false)).finally(() => setLoading(false))
|
||||
|
||||
}, [open])
|
||||
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
|
||||
style={{color: 'darkviolet'}}
|
||||
onClick={() => setOpen(true)}/>
|
||||
</Typography.Link>
|
||||
<Modal
|
||||
title={t`Watchlist Entity Diagram`}
|
||||
centered
|
||||
open={open}
|
||||
loading={loading}
|
||||
footer={
|
||||
<Space>
|
||||
<Button type="default" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onOk={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
width='90vw'
|
||||
height='100%'
|
||||
>
|
||||
<Flex style={{width: '85vw', height: '85vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
defaultEdges={[]}
|
||||
defaultNodes={[]}
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<Typography.Link>
|
||||
<ApartmentOutlined
|
||||
title={t`View the Watchlist Entity Diagram`}
|
||||
style={{color: 'darkviolet'}}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Typography.Link>
|
||||
<Modal
|
||||
title={t`Watchlist Entity Diagram`}
|
||||
centered
|
||||
open={open}
|
||||
loading={loading}
|
||||
footer={
|
||||
<Space>
|
||||
<Button type='default' onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onOk={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
width='90vw'
|
||||
height='100%'
|
||||
>
|
||||
<Flex style={{width: '85vw', height: '85vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
defaultEdges={[]}
|
||||
defaultNodes={[]}
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import dagre from "dagre"
|
||||
import dagre from 'dagre'
|
||||
import {Edge, Node, Position} from '@xyflow/react'
|
||||
|
||||
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
|
||||
export const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const nodeWidth = 172
|
||||
const nodeHeight = 200
|
||||
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({rankdir: direction});
|
||||
const isHorizontal = direction === 'LR'
|
||||
dagreGraph.setGraph({rankdir: direction})
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
|
||||
});
|
||||
nodes.forEach(node => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight})
|
||||
})
|
||||
|
||||
edges.forEach((edge: any) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
edges.forEach(edge => {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
})
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
const newNodes = nodes.map((node: any) => {
|
||||
const newNodes: Node[] = nodes.map(node => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? 'left' : 'top',
|
||||
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {nodes: newNodes, edges};
|
||||
}
|
||||
return {nodes: newNodes, edges}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import {Domain, Watchlist} from "../../../../utils/api";
|
||||
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
|
||||
import {t} from "ttag";
|
||||
import {Domain, Watchlist} from '../../../../utils/api'
|
||||
import {rdapRoleTranslation} from '../../../../utils/functions/rdapTranslation'
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
|
||||
import {rolesToColor} from '../../../../utils/functions/rolesToColor'
|
||||
import {Edge} from '@xyflow/react'
|
||||
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false): Edge[] {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
.map(e => ({
|
||||
id: `e-${d.ldhName}-${e.entity.handle}`,
|
||||
@@ -21,11 +22,11 @@ export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
label: e.roles
|
||||
.map(r => rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r)
|
||||
.join(', '),
|
||||
animated: e.roles.includes('registrant'),
|
||||
animated: e.roles.includes('registrant')
|
||||
}))
|
||||
}
|
||||
|
||||
export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
export const domainNSToEdges = (d: Domain): Edge[] => d.nameservers
|
||||
.map(ns => ({
|
||||
id: `ns-${d.ldhName}-${ns.ldhName}`,
|
||||
source: d.ldhName,
|
||||
@@ -34,7 +35,7 @@ export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
label: 'DNS'
|
||||
}))
|
||||
|
||||
export const tldToEdge = (d: Domain) => ({
|
||||
export const tldToEdge = (d: Domain): Edge => ({
|
||||
id: `tld-${d.ldhName}-${d.tld.tld}`,
|
||||
source: d.tld.tld,
|
||||
target: d.ldhName,
|
||||
@@ -42,7 +43,7 @@ export const tldToEdge = (d: Domain) => ({
|
||||
label: t`Registry`
|
||||
})
|
||||
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false): Edge[] {
|
||||
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
|
||||
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
|
||||
const tldEdge = watchlist.domains.map(tldToEdge)
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
|
||||
import React from "react";
|
||||
import {Domain, Nameserver, Tld, Watchlist} from '../../../../utils/api'
|
||||
import React from 'react'
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {entityToName} from "../../../../utils/functions/entityToName";
|
||||
import {entityToName} from '../../../../utils/functions/entityToName'
|
||||
import {Node} from '@xyflow/react'
|
||||
|
||||
export const domainToNode = (d: Domain) => ({
|
||||
export const domainToNode = (d: Domain): Node => ({
|
||||
id: d.ldhName,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: <b>{d.ldhName}</b>},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false): Node[] => {
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
.map(e => {
|
||||
return {
|
||||
id: e.entity.handle,
|
||||
position: {x: 0, y: 0},
|
||||
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
|
||||
data: {label: entityToName(e)},
|
||||
style: {
|
||||
@@ -32,8 +35,9 @@ export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const tldToNode = (tld: Tld) => ({
|
||||
export const tldToNode = (tld: Tld): Node => ({
|
||||
id: tld.tld,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: t`.${tld.tld} Registry`},
|
||||
type: 'input',
|
||||
style: {
|
||||
@@ -41,8 +45,9 @@ export const tldToNode = (tld: Tld) => ({
|
||||
}
|
||||
})
|
||||
|
||||
export const nsToNode = (ns: Nameserver) => ({
|
||||
export const nsToNode = (ns: Nameserver): Node => ({
|
||||
id: ns.ldhName,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: ns.ldhName},
|
||||
type: 'output',
|
||||
style: {
|
||||
@@ -50,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 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 nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar)
|
||||
|
||||
return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])]
|
||||
}
|
||||
}
|
||||
|
||||
6
assets/declaration.d.ts
vendored
6
assets/declaration.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module '*.md' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Breakpoint, theme} from 'antd';
|
||||
import {useMediaQuery} from 'react-responsive';
|
||||
import {Breakpoint, theme} from 'antd'
|
||||
import {useMediaQuery} from 'react-responsive'
|
||||
|
||||
const {useToken} = theme;
|
||||
const {useToken} = theme
|
||||
|
||||
type ScreenProperty = 'screenXXL' | 'screenXL' | 'screenLG' | 'screenMD' | 'screenSM' | 'screenXS';
|
||||
type ScreenProperty = 'screenXXL' | 'screenXL' | 'screenLG' | 'screenMD' | 'screenSM' | 'screenXS'
|
||||
|
||||
const propertyName = (breakpoint: Breakpoint): ScreenProperty => {
|
||||
return 'screen' + breakpoint.toUpperCase() as ScreenProperty
|
||||
};
|
||||
}
|
||||
|
||||
export default function useBreakpoint(
|
||||
breakpoint: Breakpoint
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import {HashRouter} from "react-router-dom";
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import {HashRouter} from 'react-router-dom'
|
||||
|
||||
import 'antd/dist/reset.css';
|
||||
import 'antd/dist/reset.css'
|
||||
import './i18n'
|
||||
|
||||
import './index.css'
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
function Index() {
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<App/>
|
||||
</HashRouter>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
root.render(<Index/>)
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import React, {createContext, useEffect, useState} from "react";
|
||||
import {Button, Card} from "antd";
|
||||
import React, {createContext, useEffect, useState} from 'react'
|
||||
import {Button, Card} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import TextPage from "./TextPage";
|
||||
import {LoginForm} from "../components/LoginForm";
|
||||
import {getConfiguration, InstanceConfig} from "../utils/api";
|
||||
import {RegisterForm} from "../components/RegisterForm";
|
||||
import TextPage from './TextPage'
|
||||
import {LoginForm} from '../components/LoginForm'
|
||||
import {getConfiguration, InstanceConfig} from '../utils/api'
|
||||
import {RegisterForm} from '../components/RegisterForm'
|
||||
|
||||
|
||||
export const AuthenticatedContext = createContext<any>(null)
|
||||
export const AuthenticatedContext = createContext<
|
||||
{
|
||||
authenticated: (authenticated: boolean) => void
|
||||
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
>({
|
||||
authenticated: () => {
|
||||
},
|
||||
setIsAuthenticated: () => {
|
||||
}
|
||||
})
|
||||
|
||||
export default function LoginPage() {
|
||||
|
||||
const [wantRegister, setWantRegister] = useState<boolean>(false)
|
||||
const [configuration, setConfiguration] = useState<InstanceConfig>()
|
||||
|
||||
@@ -22,19 +30,24 @@ export default function LoginPage() {
|
||||
getConfiguration().then(setConfiguration)
|
||||
}, [])
|
||||
|
||||
return <Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
|
||||
<Card.Grid style={{width: '50%', textAlign: 'center'}} hoverable={false}>
|
||||
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
|
||||
{
|
||||
configuration?.registerEnabled &&
|
||||
<Button type='link'
|
||||
return (
|
||||
<Card title={wantRegister ? t`Register` : t`Log in`} style={{width: '100%'}}>
|
||||
<Card.Grid style={{width: '50%', textAlign: 'center'}} hoverable={false}>
|
||||
{wantRegister ? <RegisterForm/> : <LoginForm ssoLogin={configuration?.ssoLogin}/>}
|
||||
{
|
||||
configuration?.registerEnabled &&
|
||||
<Button
|
||||
type='link'
|
||||
block
|
||||
style={{marginTop: '1em'}}
|
||||
onClick={toggleWantRegister}>{wantRegister ? t`Log in` : t`Create an account`}</Button>
|
||||
}
|
||||
</Card.Grid>
|
||||
<Card.Grid style={{width: '50%'}} hoverable={false}>
|
||||
<TextPage resource='ads.md'/>
|
||||
</Card.Grid>
|
||||
</Card>
|
||||
}
|
||||
onClick={toggleWantRegister}
|
||||
>{wantRegister ? t`Log in` : t`Create an account`}
|
||||
</Button>
|
||||
}
|
||||
</Card.Grid>
|
||||
<Card.Grid style={{width: '50%'}} hoverable={false}>
|
||||
<TextPage resource='ads.md'/>
|
||||
</Card.Grid>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {Result} from "antd";
|
||||
import React from "react";
|
||||
import {Result} from 'antd'
|
||||
import React from 'react'
|
||||
import {t} from 'ttag'
|
||||
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return <Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle={t`Sorry, the page you visited does not exist.`}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<Result
|
||||
status='404'
|
||||
title='404'
|
||||
subTitle={t`Sorry, the page you visited does not exist.`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {getStatistics, Statistics} from "../utils/api";
|
||||
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {getStatistics, Statistics} from '../utils/api'
|
||||
import {Card, Col, Divider, Row, Statistic, Tooltip} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {
|
||||
AimOutlined,
|
||||
CompassOutlined,
|
||||
DatabaseOutlined,
|
||||
FieldTimeOutlined,
|
||||
NotificationOutlined
|
||||
} from "@ant-design/icons";
|
||||
AimOutlined,
|
||||
CompassOutlined,
|
||||
DatabaseOutlined,
|
||||
FieldTimeOutlined,
|
||||
NotificationOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
export default function StatisticsPage() {
|
||||
|
||||
const [stats, setStats] = useState<Statistics>()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,101 +19,104 @@ export default function StatisticsPage() {
|
||||
|
||||
const totalDomainPurchase = (stats?.domainPurchased ?? 0) + (stats?.domainPurchaseFailed ?? 0)
|
||||
|
||||
const successRate = stats !== undefined ?
|
||||
(totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
|
||||
const successRate = stats !== undefined
|
||||
? (totalDomainPurchase === 0 ? undefined : stats.domainPurchased / totalDomainPurchase)
|
||||
: undefined
|
||||
|
||||
return <>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<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}>
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={`.${tld}`}
|
||||
value={domain}
|
||||
valueStyle={{color: 'darkorange'}}
|
||||
prefix={<CompassOutlined/>}
|
||||
title={t`RDAP queries`}
|
||||
value={stats?.rdapQueries}
|
||||
/>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</>
|
||||
}
|
||||
</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 key={tld} span={4}>
|
||||
<Card bordered={false}>
|
||||
<Statistic
|
||||
loading={stats === undefined}
|
||||
title={`.${tld}`}
|
||||
value={domain}
|
||||
valueStyle={{color: 'darkorange'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import snarkdown from "snarkdown"
|
||||
import {Skeleton, Typography} from "antd";
|
||||
import axios from "axios";
|
||||
import {t} from "ttag";
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import snarkdown from 'snarkdown'
|
||||
import {Skeleton, Typography} from 'antd'
|
||||
import axios from 'axios'
|
||||
import {t} from 'ttag'
|
||||
|
||||
export default function TextPage({resource}: { resource: string }) {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -12,18 +12,22 @@ export default function TextPage({resource}: { resource: string }) {
|
||||
setLoading(true)
|
||||
axios.get('/content/' + resource)
|
||||
.then(res => setMarkdown(res.data))
|
||||
.catch(err => {
|
||||
.catch(() => {
|
||||
console.error(`Please create the /public/content/${resource} file.`)
|
||||
setMarkdown(undefined)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [resource])
|
||||
|
||||
return <Skeleton loading={loading} active>
|
||||
{markdown !== undefined ? <div
|
||||
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div> :
|
||||
<Typography.Text strong>
|
||||
{t`📝 Please create the /public/content/${resource} file.`}
|
||||
</Typography.Text>}
|
||||
</Skeleton>
|
||||
}
|
||||
return (
|
||||
<Skeleton loading={loading} active>
|
||||
{markdown !== undefined
|
||||
? <div
|
||||
dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}
|
||||
/>
|
||||
: <Typography.Text strong>
|
||||
{t`📝 Please create the /public/content/${resource} file.`}
|
||||
</Typography.Text>}
|
||||
</Skeleton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Flex, Skeleton, Typography} from "antd";
|
||||
import {getUser, User} from "../utils/api";
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Card, Flex, Skeleton, Typography} from 'antd'
|
||||
import {getUser, User} from '../utils/api'
|
||||
import {t} from 'ttag'
|
||||
|
||||
export default function UserPage() {
|
||||
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(setUser)
|
||||
}, [])
|
||||
|
||||
return <Skeleton loading={user === null} active>
|
||||
<Flex gap="middle" align="center" justify="center" vertical>
|
||||
<Card title={t`My Account`}>
|
||||
<Typography.Paragraph>
|
||||
{t`Username`} : {user?.email}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph>
|
||||
{t`Roles`} : {user?.roles.join(',')}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Skeleton>
|
||||
}
|
||||
return (
|
||||
<Skeleton loading={user === null} active>
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
<Card title={t`My Account`}>
|
||||
<Typography.Paragraph>
|
||||
{t`Username`} : {user?.email}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph>
|
||||
{t`Roles`} : {user?.roles.join(',')}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Skeleton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Empty, Flex, FormProps, message, Skeleton} from "antd";
|
||||
import {Domain, getDomain} from "../../utils/api";
|
||||
import {AxiosError} from "axios"
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Empty, Flex, FormProps, message, Skeleton} from 'antd'
|
||||
import {Domain, getDomain} from '../../utils/api'
|
||||
import {AxiosError} from 'axios'
|
||||
import {t} from 'ttag'
|
||||
import {DomainSearchBar, FieldType} from "../../components/search/DomainSearchBar";
|
||||
import {DomainResult} from "../../components/search/DomainResult";
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {DomainSearchBar, FieldType} from '../../components/search/DomainSearchBar'
|
||||
import {DomainResult} from '../../components/search/DomainResult'
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
import {useNavigate, useParams} from 'react-router-dom'
|
||||
|
||||
export default function DomainSearchPage() {
|
||||
const {query} = useParams()
|
||||
@@ -36,17 +36,21 @@ export default function DomainSearchPage() {
|
||||
onFinish({ldhName: query})
|
||||
}, [])
|
||||
|
||||
return <Flex gap="middle" align="center" justify="center" vertical>
|
||||
{contextHolder}
|
||||
<DomainSearchBar initialValue={query} onFinish={onFinish}/>
|
||||
return (
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
<DomainSearchBar initialValue={query} onFinish={onFinish}/>
|
||||
|
||||
<Skeleton loading={domain === null} active>
|
||||
{
|
||||
domain &&
|
||||
(!domain.deleted ? <DomainResult domain={domain}/>
|
||||
: <Empty
|
||||
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}/>)
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
}
|
||||
<Skeleton loading={domain === null} active>
|
||||
{
|
||||
(domain != null) &&
|
||||
(!domain.deleted
|
||||
? <DomainResult domain={domain}/>
|
||||
: <Empty
|
||||
description={t`Although the domain exists in my database, it has been deleted from the WHOIS by its registrar.`}
|
||||
/>)
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
export default function EntitySearchPage() {
|
||||
return <p>
|
||||
Not implemented
|
||||
</p>
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
Not implemented
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
export default function NameserverSearchPage() {
|
||||
return <p>
|
||||
Not implemented
|
||||
</p>
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
Not implemented
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,53 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Collapse, Divider, Table, Typography} from "antd";
|
||||
import {getTldList, Tld} from "../../utils/api";
|
||||
import React, {ReactElement, useEffect, useState} from 'react'
|
||||
import {Collapse, Divider, Table, Typography} from 'antd'
|
||||
import {getTldList, Tld} from '../../utils/api'
|
||||
import {t} from 'ttag'
|
||||
import {regionNames} from "../../i18n";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import punycode from "punycode/punycode";
|
||||
import {getCountryCode} from "../../utils/functions/getCountryCode";
|
||||
import {tldToEmoji} from "../../utils/functions/tldToEmoji";
|
||||
import {regionNames} from '../../i18n'
|
||||
import useBreakpoint from '../../hooks/useBreakpoint'
|
||||
import {ColumnType} from 'antd/es/table'
|
||||
import punycode from 'punycode/punycode'
|
||||
import {getCountryCode} from '../../utils/functions/getCountryCode'
|
||||
import {tldToEmoji} from '../../utils/functions/tldToEmoji'
|
||||
|
||||
const {Text, Paragraph} = Typography
|
||||
|
||||
type TldType = 'iTLD' | 'sTLD' | 'gTLD' | 'ccTLD'
|
||||
type FiltersType = { type: TldType, contractTerminated?: boolean, specification13?: boolean }
|
||||
|
||||
interface FiltersType {
|
||||
type: TldType,
|
||||
contractTerminated?: boolean,
|
||||
specification13?: boolean
|
||||
}
|
||||
|
||||
function TldTable(filters: FiltersType) {
|
||||
interface TableRow {
|
||||
key: string
|
||||
TLD: ReactElement
|
||||
Flag?: string
|
||||
Country?: string
|
||||
}
|
||||
|
||||
const sm = useBreakpoint('sm')
|
||||
const [dataTable, setDataTable] = useState<Tld[]>([])
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const fetchData = (params: FiltersType & { page: number, itemsPerPage: number }) => {
|
||||
getTldList(params).then((data) => {
|
||||
setTotal(data['hydra:totalItems'])
|
||||
setDataTable(data['hydra:member'].map((tld: Tld) => {
|
||||
|
||||
const rowData = {
|
||||
key: tld.tld,
|
||||
TLD: <Typography.Text code>{punycode.toUnicode(tld.tld)}</Typography.Text>
|
||||
}
|
||||
switch (filters.type) {
|
||||
const type = filters.type
|
||||
let countryName
|
||||
|
||||
switch (type) {
|
||||
case 'ccTLD':
|
||||
let countryName
|
||||
|
||||
try {
|
||||
countryName = regionNames.of(getCountryCode(tld.tld))
|
||||
} catch (e) {
|
||||
} catch {
|
||||
countryName = '-'
|
||||
}
|
||||
|
||||
@@ -60,98 +72,104 @@ function TldTable(filters: FiltersType) {
|
||||
fetchData({...filters, page: 1, itemsPerPage: 30})
|
||||
}, [])
|
||||
|
||||
let columns: ColumnType<any>[] = [
|
||||
let columns: Array<ColumnType<TableRow>> = [
|
||||
{
|
||||
title: t`TLD`,
|
||||
dataIndex: "TLD"
|
||||
dataIndex: 'TLD'
|
||||
}
|
||||
]
|
||||
|
||||
if (filters.type === 'ccTLD') columns = [...columns, {
|
||||
title: t`Flag`,
|
||||
dataIndex: "Flag",
|
||||
}, {
|
||||
title: t`Country`,
|
||||
dataIndex: "Country"
|
||||
}]
|
||||
if (filters.type === 'ccTLD') {
|
||||
columns = [...columns, {
|
||||
title: t`Flag`,
|
||||
dataIndex: 'Flag'
|
||||
}, {
|
||||
title: t`Country`,
|
||||
dataIndex: 'Country'
|
||||
}]
|
||||
}
|
||||
|
||||
if (filters.type === 'gTLD') columns = [...columns, {
|
||||
title: t`Registry Operator`,
|
||||
dataIndex: "Operator"
|
||||
}]
|
||||
if (filters.type === 'gTLD') {
|
||||
columns = [...columns, {
|
||||
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
|
||||
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}})}
|
||||
/>
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default function TldPage() {
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
return <>
|
||||
<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.
|
||||
return (
|
||||
<>
|
||||
<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.
|
||||
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).
|
||||
At the same time, the list of root RDAP servers is updated.`}
|
||||
</Paragraph>
|
||||
<Divider/>
|
||||
<Collapse
|
||||
accordion
|
||||
size={sm ? 'small' : 'large'}
|
||||
items={[
|
||||
{
|
||||
key: 'sTLD',
|
||||
label: t`Sponsored Top-Level-Domains`,
|
||||
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>
|
||||
<Divider/>
|
||||
<TldTable type='sTLD'/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'gTLD',
|
||||
label: t`Generic Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Generic top-level domains open to everyone, not restricted by specific criteria, representing various themes or industries.`}</Text>
|
||||
<Divider/>
|
||||
<TldTable type='gTLD' contractTerminated={false} specification13={false}/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'ngTLD',
|
||||
label: t`Brand Generic Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Generic top-level domains associated with specific brands, allowing companies to use their own brand names as domains.`}</Text>
|
||||
<Divider/>
|
||||
<TldTable type='gTLD' contractTerminated={false} specification13={true}/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'ccTLD',
|
||||
label: t`Country-Code Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Top-level domains based on country codes, identifying websites according to their country of origin.`}</Text>
|
||||
<Divider/><TldTable type='ccTLD'/>
|
||||
</>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Paragraph>
|
||||
<Divider/>
|
||||
<Collapse
|
||||
accordion
|
||||
size={sm ? 'small' : 'large'}
|
||||
items={[
|
||||
{
|
||||
key: 'sTLD',
|
||||
label: t`Sponsored Top-Level-Domains`,
|
||||
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>
|
||||
<Divider/>
|
||||
<TldTable type='sTLD'/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'gTLD',
|
||||
label: t`Generic Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Generic top-level domains open to everyone, not restricted by specific criteria, representing various themes or industries.`}</Text>
|
||||
<Divider/>
|
||||
<TldTable type='gTLD' contractTerminated={false} specification13={false}/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'ngTLD',
|
||||
label: t`Brand Generic Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Generic top-level domains associated with specific brands, allowing companies to use their own brand names as domains.`}</Text>
|
||||
<Divider/>
|
||||
<TldTable type='gTLD' contractTerminated={false} specification13/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
key: 'ccTLD',
|
||||
label: t`Country-Code Top-Level-Domains`,
|
||||
children: <>
|
||||
<Text>{t`Top-level domains based on country codes, identifying websites according to their country of origin.`}</Text>
|
||||
<Divider/><TldTable type='ccTLD'/>
|
||||
</>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Flex, Form, message, Skeleton} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {Connector, getConnectors, postConnector} from "../../utils/api/connectors";
|
||||
import {ConnectorForm} from "../../components/tracking/connector/ConnectorForm";
|
||||
import {AxiosError} from "axios";
|
||||
import {ConnectorElement, ConnectorsList} from "../../components/tracking/connector/ConnectorsList";
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Card, Flex, Form, message, Skeleton} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {Connector, getConnectors, postConnector} from '../../utils/api/connectors'
|
||||
import {ConnectorForm} from '../../components/tracking/connector/ConnectorForm'
|
||||
import {AxiosError} from 'axios'
|
||||
import {ConnectorElement, ConnectorsList} from '../../components/tracking/connector/ConnectorsList'
|
||||
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
|
||||
export default function ConnectorPage() {
|
||||
const [form] = Form.useForm()
|
||||
@@ -14,7 +14,7 @@ export default function ConnectorPage() {
|
||||
const [connectors, setConnectors] = useState<ConnectorElement[] | null>()
|
||||
|
||||
const onCreateConnector = (values: Connector) => {
|
||||
postConnector(values).then((w) => {
|
||||
postConnector(values).then(() => {
|
||||
form.resetFields()
|
||||
refreshConnectors()
|
||||
messageApi.success(t`Connector created !`)
|
||||
@@ -23,7 +23,7 @@ export default function ConnectorPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const refreshConnectors = () => getConnectors().then(c => {
|
||||
const refreshConnectors = async () => await getConnectors().then(c => {
|
||||
setConnectors(c['hydra:member'])
|
||||
}).catch((e: AxiosError) => {
|
||||
setConnectors(undefined)
|
||||
@@ -34,18 +34,17 @@ export default function ConnectorPage() {
|
||||
refreshConnectors()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
<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>
|
||||
<Card title={t`Create a Connector`} style={{width: '100%'}}>
|
||||
{contextHolder}
|
||||
<ConnectorForm form={form} onCreate={onCreateConnector}/>
|
||||
</Card>
|
||||
|
||||
|
||||
<Skeleton loading={connectors === undefined} active>
|
||||
{connectors && connectors.length > 0 &&
|
||||
<ConnectorsList connectors={connectors} onDelete={refreshConnectors}/>
|
||||
}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
}
|
||||
<Skeleton loading={connectors === undefined} active>
|
||||
{(connectors != null) && connectors.length > 0 &&
|
||||
<ConnectorsList connectors={connectors} onDelete={refreshConnectors}/>}
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {TrackedDomainTable} from "../../components/tracking/watchlist/TrackedDomainTable";
|
||||
import React from "react";
|
||||
import {TrackedDomainTable} from '../../components/tracking/watchlist/TrackedDomainTable'
|
||||
import React from 'react'
|
||||
|
||||
export default function TrackedDomainPage() {
|
||||
return <TrackedDomainTable/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Card, Divider, Flex, Form, message} from "antd";
|
||||
import {EventAction, getWatchlists, postWatchlist, putWatchlist} from "../../utils/api";
|
||||
import {AxiosError} from "axios";
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {Card, Divider, Flex, Form, message} from 'antd'
|
||||
import {getWatchlists, postWatchlist, putWatchlist, Watchlist} from '../../utils/api'
|
||||
import {AxiosError} from 'axios'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from "../../components/tracking/watchlist/WatchlistForm";
|
||||
import {WatchlistsList} from "../../components/tracking/watchlist/WatchlistsList";
|
||||
import {Connector, getConnectors} from "../../utils/api/connectors";
|
||||
import {WatchlistForm} from '../../components/tracking/watchlist/WatchlistForm'
|
||||
import {WatchlistsList} from '../../components/tracking/watchlist/WatchlistsList'
|
||||
import {Connector, getConnectors} from '../../utils/api/connectors'
|
||||
|
||||
import {showErrorAPI} from "../../utils/functions/showErrorAPI";
|
||||
import {showErrorAPI} from '../../utils/functions/showErrorAPI'
|
||||
|
||||
|
||||
export type Watchlist = {
|
||||
interface FormValuesType {
|
||||
name?: string
|
||||
token: string,
|
||||
domains: { ldhName: string, deleted: boolean, status: string[] }[],
|
||||
triggers?: { event: EventAction, action: string }[],
|
||||
dsn?: string[]
|
||||
connector?: {
|
||||
id: string
|
||||
provider: string
|
||||
createdAt: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type FormValuesType = {
|
||||
name?: string
|
||||
domains: string[],
|
||||
domains: string[]
|
||||
triggers: string[]
|
||||
connector?: string,
|
||||
connector?: string
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
@@ -52,14 +37,13 @@ const getRequestDataFromForm = (values: FormValuesType) => {
|
||||
}
|
||||
|
||||
export default function WatchlistPage() {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [watchlists, setWatchlists] = useState<Watchlist[]>()
|
||||
const [connectors, setConnectors] = useState<(Connector & { id: string })[]>()
|
||||
const [connectors, setConnectors] = useState<Array<Connector & { id: string }>>()
|
||||
|
||||
const onCreateWatchlist = (values: FormValuesType) => {
|
||||
postWatchlist(getRequestDataFromForm(values)).then((w) => {
|
||||
postWatchlist(getRequestDataFromForm(values)).then(() => {
|
||||
form.resetFields()
|
||||
refreshWatchlists()
|
||||
messageApi.success(t`Watchlist created !`)
|
||||
@@ -68,18 +52,18 @@ export default function WatchlistPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => putWatchlist({
|
||||
const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => await putWatchlist({
|
||||
token: values.token,
|
||||
...getRequestDataFromForm(values)
|
||||
}
|
||||
).then((w) => {
|
||||
).then(() => {
|
||||
refreshWatchlists()
|
||||
messageApi.success(t`Watchlist updated !`)
|
||||
}).catch((e: AxiosError) => {
|
||||
throw showErrorAPI(e, messageApi)
|
||||
})
|
||||
|
||||
const refreshWatchlists = () => getWatchlists().then(w => {
|
||||
const refreshWatchlists = async () => await getWatchlists().then(w => {
|
||||
setWatchlists(w['hydra:member'])
|
||||
}).catch((e: AxiosError) => {
|
||||
setWatchlists(undefined)
|
||||
@@ -95,18 +79,20 @@ export default function WatchlistPage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
return <Flex gap="middle" align="center" justify="center" vertical>
|
||||
{contextHolder}
|
||||
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
|
||||
{connectors &&
|
||||
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation={true}/>
|
||||
}
|
||||
</Card>
|
||||
<Divider/>
|
||||
{connectors && watchlists && watchlists.length > 0 &&
|
||||
<WatchlistsList watchlists={watchlists} onDelete={refreshWatchlists}
|
||||
connectors={connectors}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
/>}
|
||||
</Flex>
|
||||
}
|
||||
return (
|
||||
<Flex gap='middle' align='center' justify='center' vertical>
|
||||
{contextHolder}
|
||||
<Card loading={connectors === undefined} title={t`Create a Watchlist`} style={{width: '100%'}}>
|
||||
{(connectors != null) &&
|
||||
<WatchlistForm form={form} onFinish={onCreateWatchlist} connectors={connectors} isCreation/>}
|
||||
</Card>
|
||||
<Divider/>
|
||||
{(connectors != null) && (watchlists != null) && watchlists.length > 0 &&
|
||||
<WatchlistsList
|
||||
watchlists={watchlists} onDelete={refreshWatchlists}
|
||||
connectors={connectors}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
/>}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {request} from "./index";
|
||||
import {request} from './index'
|
||||
import {ConnectorElement} from '../../components/tracking/connector/ConnectorsList'
|
||||
|
||||
export enum ConnectorProvider {
|
||||
OVH = 'ovh',
|
||||
@@ -7,13 +8,18 @@ export enum ConnectorProvider {
|
||||
NAMECHEAP = 'namecheap'
|
||||
}
|
||||
|
||||
export type Connector = {
|
||||
export interface Connector {
|
||||
provider: ConnectorProvider
|
||||
authData: object
|
||||
}
|
||||
|
||||
export async function getConnectors() {
|
||||
const response = await request({
|
||||
interface ConnectorResponse {
|
||||
'hydra:totalItems': number
|
||||
'hydra:member': ConnectorElement[]
|
||||
}
|
||||
|
||||
export async function getConnectors(): Promise<ConnectorResponse> {
|
||||
const response = await request<ConnectorResponse>({
|
||||
url: 'connectors'
|
||||
})
|
||||
return response.data
|
||||
@@ -25,7 +31,7 @@ export async function postConnector(connector: Connector) {
|
||||
url: 'connectors',
|
||||
data: connector,
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Domain, request} from ".";
|
||||
|
||||
import {Domain, request} from '.'
|
||||
|
||||
export async function getDomain(ldhName: string): Promise<Domain> {
|
||||
const response = await request<Domain>({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import axios, {AxiosRequestConfig, AxiosResponse} from "axios";
|
||||
|
||||
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'
|
||||
|
||||
export type EventAction =
|
||||
'registration'
|
||||
@@ -26,7 +25,12 @@ export interface Event {
|
||||
|
||||
export interface Entity {
|
||||
handle: string
|
||||
jCard: any
|
||||
jCard: ['vcard', Array<[
|
||||
string,
|
||||
{ [key: string]: string | string[] },
|
||||
string,
|
||||
string | string[],
|
||||
]>] | []
|
||||
}
|
||||
|
||||
export interface Nameserver {
|
||||
@@ -50,12 +54,12 @@ export interface Domain {
|
||||
handle: string
|
||||
status: string[]
|
||||
events: Event[]
|
||||
entities: {
|
||||
entities: Array<{
|
||||
entity: Entity
|
||||
events: Event[]
|
||||
roles: string[]
|
||||
deleted: boolean
|
||||
}[]
|
||||
}>
|
||||
nameservers: Nameserver[]
|
||||
tld: Tld
|
||||
deleted: boolean
|
||||
@@ -70,20 +74,24 @@ export interface User {
|
||||
|
||||
export interface WatchlistRequest {
|
||||
name?: string
|
||||
domains: string[],
|
||||
triggers: { event: EventAction, action: TriggerAction }[],
|
||||
domains: string[]
|
||||
triggers: Array<{ event: EventAction, action: TriggerAction }>
|
||||
connector?: string
|
||||
dsn?: string[]
|
||||
}
|
||||
|
||||
export interface Watchlist {
|
||||
token: string
|
||||
name?: string
|
||||
domains: Domain[],
|
||||
triggers: { event: EventAction, action: TriggerAction }[],
|
||||
connector?: string
|
||||
createdAt: string
|
||||
token: string
|
||||
domains: Domain[]
|
||||
triggers?: Array<{ event: EventAction, action: string }>
|
||||
dsn?: string[]
|
||||
connector?: {
|
||||
id: string
|
||||
provider: string
|
||||
createdAt: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface InstanceConfig {
|
||||
@@ -97,28 +105,30 @@ export interface Statistics {
|
||||
alertSent: number
|
||||
domainPurchased: number
|
||||
domainPurchaseFailed: number
|
||||
domainCount: {tld: string, domain: number}[]
|
||||
domainCount: Array<{ tld: string, domain: number }>
|
||||
domainCountTotal: number
|
||||
domainTracked: number
|
||||
}
|
||||
|
||||
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
|
||||
export interface TrackedDomains {
|
||||
'hydra:totalItems': number
|
||||
'hydra:member': Domain[]
|
||||
}
|
||||
|
||||
export async function request<T = object, R = AxiosResponse<T>, D = object>(config: AxiosRequestConfig): Promise<R> {
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
...config,
|
||||
baseURL: '/api',
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Accept: 'application/ld+json',
|
||||
...config.headers,
|
||||
...config.headers
|
||||
}
|
||||
}
|
||||
return await axios.request<T, R, D>(axiosConfig)
|
||||
}
|
||||
|
||||
|
||||
export * from './domain'
|
||||
export * from './tld'
|
||||
export * from './user'
|
||||
export * from './watchlist'
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {request} from "./index";
|
||||
import {request, Tld} from './index'
|
||||
|
||||
interface Tld {
|
||||
tld: string
|
||||
contractTerminated: boolean
|
||||
registryOperator: string
|
||||
specification13: boolean
|
||||
interface TldList {
|
||||
'hydra:totalItems': number
|
||||
'hydra:member': Tld[]
|
||||
}
|
||||
|
||||
export async function getTldList(params: object): Promise<any> {
|
||||
return (await request<Tld[]>({
|
||||
export async function getTldList(params: object): Promise<TldList> {
|
||||
return (await request<TldList>({
|
||||
url: 'tld',
|
||||
params,
|
||||
params
|
||||
})).data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {InstanceConfig, request, Statistics, User} from "./index";
|
||||
|
||||
import {InstanceConfig, request, Statistics, User} from './index'
|
||||
|
||||
export async function login(email: string, password: string): Promise<boolean> {
|
||||
const response = await request({
|
||||
@@ -19,7 +18,6 @@ export async function register(email: string, password: string): Promise<boolean
|
||||
return response.status === 201
|
||||
}
|
||||
|
||||
|
||||
export async function getUser(): Promise<User> {
|
||||
const response = await request<User>({
|
||||
url: 'me'
|
||||
@@ -39,4 +37,4 @@ export async function getStatistics(): Promise<Statistics> {
|
||||
url: 'stats'
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import {Domain, request, Watchlist, WatchlistRequest} from "./index";
|
||||
import {request, TrackedDomains, Watchlist, WatchlistRequest} from './index'
|
||||
|
||||
export async function getWatchlists() {
|
||||
const response = await request({
|
||||
interface WatchlistList {
|
||||
'hydra:totalItems': number
|
||||
'hydra:member': Watchlist[]
|
||||
}
|
||||
|
||||
export async function getWatchlists(): Promise<WatchlistList> {
|
||||
const response = await request<WatchlistList>({
|
||||
url: 'watchlists'
|
||||
})
|
||||
return response.data
|
||||
@@ -20,7 +25,7 @@ export async function postWatchlist(watchlist: WatchlistRequest) {
|
||||
url: 'watchlists',
|
||||
data: watchlist,
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
@@ -37,17 +42,16 @@ export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { toke
|
||||
const response = await request<WatchlistRequest>({
|
||||
method: 'PUT',
|
||||
url: 'watchlists/' + watchlist.token,
|
||||
data: watchlist,
|
||||
data: watchlist
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<any> {
|
||||
const response = await request({
|
||||
export async function getTrackedDomainList(params: { page: number, itemsPerPage: number }): Promise<TrackedDomains> {
|
||||
const response = await request<TrackedDomains>({
|
||||
method: 'GET',
|
||||
url: 'tracked',
|
||||
params
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import {EventAction} from "../api";
|
||||
import {EventAction} from '../api'
|
||||
|
||||
export const actionToColor = (a: EventAction) => a === 'registration' ? 'green' :
|
||||
a === 'reregistration' ? 'cyan' :
|
||||
a === 'expiration' ? 'red' :
|
||||
a === 'deletion' ? 'magenta' :
|
||||
a === 'transfer' ? 'orange' :
|
||||
a === 'last changed' ? 'blue' :
|
||||
a === 'registrar expiration' ? 'red' :
|
||||
a === 'reinstantiation' ? 'purple' :
|
||||
a === 'enum validation expiration' ? 'red' : 'default'
|
||||
export const actionToColor = (a: EventAction) => a === 'registration'
|
||||
? 'green'
|
||||
: a === 'reregistration'
|
||||
? 'cyan'
|
||||
: a === 'expiration'
|
||||
? 'red'
|
||||
: a === 'deletion'
|
||||
? 'magenta'
|
||||
: a === 'transfer'
|
||||
? 'orange'
|
||||
: a === 'last changed'
|
||||
? 'blue'
|
||||
: a === 'registrar expiration'
|
||||
? 'red'
|
||||
: a === 'reinstantiation'
|
||||
? 'purple'
|
||||
: a === 'enum validation expiration' ? 'red' : 'default'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {EventAction} from "../api";
|
||||
import {EventAction} from '../api'
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
@@ -9,20 +9,31 @@ import {
|
||||
SignatureOutlined,
|
||||
SyncOutlined,
|
||||
UnlockOutlined
|
||||
} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
export const actionToIcon = (a: EventAction) => a === 'registration' ?
|
||||
<SignatureOutlined style={{fontSize: '16px'}}/> : a === 'expiration' ?
|
||||
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'transfer' ?
|
||||
<ShareAltOutlined style={{fontSize: '16px'}}/> : a === 'last changed' ?
|
||||
<SyncOutlined style={{fontSize: '16px'}}/> : a === 'deletion' ?
|
||||
<DeleteOutlined style={{fontSize: '16px'}}/> : a === 'reregistration' ?
|
||||
<ReloadOutlined style={{fontSize: '16px'}}/> : a === 'locked' ?
|
||||
<LockOutlined style={{fontSize: '16px'}}/> : a === 'unlocked' ?
|
||||
<UnlockOutlined style={{fontSize: '16px'}}/> : a === 'registrar expiration' ?
|
||||
<ClockCircleOutlined
|
||||
style={{fontSize: '16px'}}/> : a === 'enum validation expiration' ?
|
||||
<ClockCircleOutlined style={{fontSize: '16px'}}/> : a === 'reinstantiation' ?
|
||||
<ReloadOutlined style={{fontSize: '16px'}}/> :
|
||||
<PushpinOutlined style={{fontSize: '16px'}}/>
|
||||
export const actionToIcon = (a: EventAction) => a === 'registration'
|
||||
? <SignatureOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'expiration'
|
||||
? <ClockCircleOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'transfer'
|
||||
? <ShareAltOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'last changed'
|
||||
? <SyncOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'deletion'
|
||||
? <DeleteOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'reregistration'
|
||||
? <ReloadOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'locked'
|
||||
? <LockOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'unlocked'
|
||||
? <UnlockOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'registrar expiration'
|
||||
? <ClockCircleOutlined
|
||||
style={{fontSize: '16px'}}
|
||||
/>
|
||||
: a === 'enum validation expiration'
|
||||
? <ClockCircleOutlined style={{fontSize: '16px'}}/>
|
||||
: a === 'reinstantiation'
|
||||
? <ReloadOutlined style={{fontSize: '16px'}}/>
|
||||
: <PushpinOutlined style={{fontSize: '16px'}}/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Entity} from "../api";
|
||||
import vCard from "vcf";
|
||||
import {Entity} from '../api'
|
||||
import vCard from 'vcf'
|
||||
|
||||
export const entityToName = (e: { entity: Entity }): string => {
|
||||
if (e.entity.jCard.length === 0) return e.entity.handle
|
||||
@@ -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()
|
||||
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const eppStatusCodeToColor = (s: string) =>
|
||||
['active', 'ok'].includes(s) ? 'green' :
|
||||
['pending delete', 'redemption period'].includes(s) ? 'red' :
|
||||
s.startsWith('client') ? 'purple' :
|
||||
s.startsWith('server') ? 'geekblue' : 'blue'
|
||||
['active', 'ok'].includes(s)
|
||||
? 'green'
|
||||
: ['pending delete', 'redemption period'].includes(s)
|
||||
? 'red'
|
||||
: s.startsWith('client')
|
||||
? 'purple'
|
||||
: s.startsWith('server') ? 'geekblue' : 'blue'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import vCard from "vcf";
|
||||
import {Entity} from "../api";
|
||||
import vCard from 'vcf'
|
||||
import {Entity} from '../api'
|
||||
|
||||
export const extractDetailsFromJCard = (e: { entity: Entity }): {
|
||||
fn?: string
|
||||
organization?: string;
|
||||
organization?: string
|
||||
} => {
|
||||
if (e.entity.jCard.length === 0) return {fn: e.entity.handle}
|
||||
const jCard = vCard.fromJSON(e.entity.jCard)
|
||||
@@ -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
|
||||
|
||||
return {fn, organization}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ export const getCountryCode = (tld: string): string => {
|
||||
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
|
||||
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
|
||||
return tld.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {t} from "ttag";
|
||||
import {t} from 'ttag'
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
@@ -17,7 +17,6 @@ export const rdapRoleTranslation = () => ({
|
||||
noc: t`Noc`
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
@@ -35,7 +34,6 @@ export const rdapRoleDetailTranslation = () => ({
|
||||
noc: t`The entity object instance handles communications related to a network operations center (NOC).`
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/rdap-json-values/rdap-json-values.xhtml
|
||||
*/
|
||||
@@ -75,20 +73,20 @@ export const rdapEventDetailTranslation = () => ({
|
||||
* @see https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
|
||||
*/
|
||||
export const rdapStatusCodeDetailTranslation = () => ({
|
||||
'validated': t`Signifies that the data of the object instance has been found to be accurate.`,
|
||||
validated: t`Signifies that the data of the object instance has been found to be accurate.`,
|
||||
'renew prohibited': t`Renewal or reregistration of the object instance is forbidden.`,
|
||||
'update prohibited': t`Updates to the object instance are forbidden.`,
|
||||
'transfer prohibited': t`Transfers of the registration from one registrar to another are forbidden.`,
|
||||
'delete prohibited': t`Deletion of the registration of the object instance is forbidden.`,
|
||||
'proxy': t`The registration of the object instance has been performed by a third party.`,
|
||||
'private': t`The information of the object instance is not designated for public consumption.`,
|
||||
'removed': t`Some of the information of the object instance has not been made available and has been removed.`,
|
||||
'obscured': t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
|
||||
'associated': t`The object instance is associated with other object instances in the registry.`,
|
||||
'locked': t`Changes to the object instance cannot be made, including the association of other object instances.`,
|
||||
proxy: t`The registration of the object instance has been performed by a third party.`,
|
||||
private: t`The information of the object instance is not designated for public consumption.`,
|
||||
removed: t`Some of the information of the object instance has not been made available and has been removed.`,
|
||||
obscured: t`Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance.`,
|
||||
associated: t`The object instance is associated with other object instances in the registry.`,
|
||||
locked: t`Changes to the object instance cannot be made, including the association of other object instances.`,
|
||||
|
||||
'active': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
'inactive': t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
|
||||
active: t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
inactive: t`This status code indicates that delegation information (name servers) has not been associated with your domain. Your domain is not activated in the DNS and will not resolve.`,
|
||||
'pending create': t`This status code indicates that a request to create your domain has been received and is being processed.`,
|
||||
'pending renew': t`This status code indicates that a request to renew your domain has been received and is being processed.`,
|
||||
'pending transfer': t`This status code indicates that a request to transfer your domain to a new registrar has been received and is being processed.`,
|
||||
@@ -96,7 +94,7 @@ export const rdapStatusCodeDetailTranslation = () => ({
|
||||
'pending delete': t`This status code may be mixed with redemptionPeriod or pendingRestore. In such case, depending on the status (i.e. redemptionPeriod or pendingRestore) set in the domain name, the corresponding description presented above applies. If this status is not combined with the redemptionPeriod or pendingRestore status, the pendingDelete status code indicates that your domain has been in redemptionPeriod status for 30 days and you have not restored it within that 30-day period. Your domain will remain in this status for several days, after which time your domain will be purged and dropped from the registry database. Once deletion occurs, the domain is available for re-registration in accordance with the registry's policies.`,
|
||||
'add period': t`This grace period is provided after the initial registration of a domain name. If the registrar deletes the domain name during this period, the registry may provide credit to the registrar for the cost of the registration.`,
|
||||
'auto renew period': t`This grace period is provided after a domain name registration period expires and is extended (renewed) automatically by the registry. If the registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the renewal.`,
|
||||
'ok': t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
ok: t`This is the standard status for a domain, meaning it has no pending operations or prohibitions.`,
|
||||
'client delete prohibited': t`This status code tells your domain's registry to reject requests to delete the domain.`,
|
||||
'client hold': t`This status code tells your domain's registry to not activate your domain in the DNS and as a consequence, it will not resolve. It is an uncommon status that is usually enacted during legal disputes, non-payment, or when your domain is subject to deletion.`,
|
||||
'client renew prohibited': t`This status code tells your domain's registry to reject requests to renew your domain. It is an uncommon status that is usually enacted during legal disputes or when your domain is subject to deletion.`,
|
||||
@@ -112,6 +110,6 @@ export const rdapStatusCodeDetailTranslation = () => ({
|
||||
'server hold': t`This status code is set by your domain's Registry Operator. Your domain is not activated in the DNS.`,
|
||||
'transfer period': t`This grace period is provided after the successful transfer of a domain name from one registrar to another. If the new registrar deletes the domain name during this period, the registry provides a credit to the registrar for the cost of the transfer.`,
|
||||
|
||||
'administrative': t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
|
||||
'reserved': t`The object instance has been allocated to an IANA special-purpose address registry.`,
|
||||
administrative: t`The object instance has been allocated administratively (i.e., not for use by the recipient in their own right in operational networks).`,
|
||||
reserved: t`The object instance has been allocated to an IANA special-purpose address registry.`
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Avatar} from "antd";
|
||||
import {Avatar} from 'antd'
|
||||
import {
|
||||
BankOutlined,
|
||||
DollarOutlined,
|
||||
@@ -6,20 +6,22 @@ import {
|
||||
SignatureOutlined,
|
||||
ToolOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
import {rolesToColor} from "./rolesToColor";
|
||||
import {rolesToColor} from './rolesToColor'
|
||||
|
||||
export const roleToAvatar = (e: { roles: string[] }) => <Avatar style={{backgroundColor: rolesToColor(e.roles)}}
|
||||
icon={e.roles.includes('registrant') ?
|
||||
<SignatureOutlined/> :
|
||||
e.roles.includes('registrar') ?
|
||||
<BankOutlined/> :
|
||||
e.roles.includes('administrative') ?
|
||||
<IdcardOutlined/> :
|
||||
e.roles.includes('technical') ?
|
||||
<ToolOutlined/> :
|
||||
e.roles.includes('billing') ?
|
||||
<DollarOutlined/> :
|
||||
<UserOutlined/>}/>
|
||||
export const roleToAvatar = (e: { roles: string[] }) => <Avatar
|
||||
style={{backgroundColor: rolesToColor(e.roles)}}
|
||||
icon={e.roles.includes('registrant')
|
||||
? <SignatureOutlined/>
|
||||
: e.roles.includes('registrar')
|
||||
? <BankOutlined/>
|
||||
: e.roles.includes('administrative')
|
||||
? <IdcardOutlined/>
|
||||
: e.roles.includes('technical')
|
||||
? <ToolOutlined/>
|
||||
: e.roles.includes('billing')
|
||||
? <DollarOutlined/>
|
||||
: <UserOutlined/>}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
export const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' :
|
||||
roles.includes('registrar') ? 'purple' :
|
||||
roles.includes('administrative') ? 'blue' :
|
||||
roles.includes('technical') ? 'orange' :
|
||||
roles.includes('sponsor') ? 'magenta' :
|
||||
roles.includes('billing') ? 'cyan' : 'default'
|
||||
export const rolesToColor = (roles: string[]) => roles.includes('registrant')
|
||||
? 'green'
|
||||
: roles.includes('registrar')
|
||||
? 'purple'
|
||||
: roles.includes('administrative')
|
||||
? 'blue'
|
||||
: roles.includes('technical')
|
||||
? 'orange'
|
||||
: roles.includes('sponsor')
|
||||
? 'magenta'
|
||||
: roles.includes('billing') ? 'cyan' : 'default'
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {AxiosError, AxiosResponse} from "axios";
|
||||
import {MessageInstance, MessageType} from "antd/lib/message/interface";
|
||||
import {t} from "ttag";
|
||||
import {AxiosError, AxiosResponse} from 'axios'
|
||||
import {MessageInstance, MessageType} from 'antd/lib/message/interface'
|
||||
import {t} from 'ttag'
|
||||
|
||||
export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): MessageType | undefined {
|
||||
|
||||
const response = e.response as AxiosResponse
|
||||
const data = response.data
|
||||
|
||||
@@ -24,4 +23,4 @@ export function showErrorAPI(e: AxiosError, messageApi: MessageInstance): Messag
|
||||
}
|
||||
|
||||
return messageApi.error(detail !== '' ? detail : t`An error occurred`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Domain} from "../api";
|
||||
import {Domain} from '../api'
|
||||
|
||||
export const sortDomainEntities = (domain: Domain) => domain.entities
|
||||
.filter(e => !e.deleted)
|
||||
.sort((e1, e2) => {
|
||||
const p = (r: string[]) => r.includes('registrant') ? 5 :
|
||||
r.includes('administrative') ? 4 :
|
||||
r.includes('billing') ? 3 :
|
||||
r.includes('registrar') ? 2 : 1
|
||||
const p = (r: string[]) => r.includes('registrant')
|
||||
? 5
|
||||
: r.includes('administrative')
|
||||
? 4
|
||||
: r.includes('billing')
|
||||
? 3
|
||||
: r.includes('registrar') ? 2 : 1
|
||||
return p(e2.roles) - p(e1.roles)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {getCountryCode} from "./getCountryCode";
|
||||
import {getCountryCode} from './getCountryCode'
|
||||
|
||||
export const tldToEmoji = (tld: string) => {
|
||||
if (tld.startsWith('xn--')) return '-'
|
||||
@@ -9,4 +9,4 @@ export const tldToEmoji = (tld: string) => {
|
||||
.split('')
|
||||
.map((char) => 127397 + char.charCodeAt(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
import {ConnectorProvider} from "../api/connectors";
|
||||
import {Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import React from "react";
|
||||
import {ConnectorProvider} from '../api/connectors'
|
||||
import {Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React from 'react'
|
||||
|
||||
export const helpGetTokenLink = (provider?: string) => {
|
||||
switch (provider) {
|
||||
case ConnectorProvider.OVH:
|
||||
return <Typography.Link target='_blank'
|
||||
href="https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions">
|
||||
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
|
||||
</Typography.Link>
|
||||
return (
|
||||
<Typography.Link
|
||||
target='_blank'
|
||||
href='https://api.ovh.com/createToken/?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*&GET=/domain/extensions'
|
||||
>
|
||||
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
|
||||
</Typography.Link>
|
||||
)
|
||||
|
||||
case ConnectorProvider.GANDI:
|
||||
return <Typography.Link target='_blank' href="https://admin.gandi.net/organizations/account/pat">
|
||||
{t`Retrieve a Personal Access Token from your customer account on the Provider's website`}
|
||||
</Typography.Link>
|
||||
return (
|
||||
<Typography.Link target='_blank' href='https://admin.gandi.net/organizations/account/pat'>
|
||||
{t`Retrieve a Personal Access Token from your customer account on the Provider's website`}
|
||||
</Typography.Link>
|
||||
)
|
||||
case ConnectorProvider.NAMECHEAP:
|
||||
return <Typography.Link target='_blank' href="https://ap.www.namecheap.com/settings/tools/apiaccess/">
|
||||
{t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`}
|
||||
</Typography.Link>
|
||||
return (
|
||||
<Typography.Link target='_blank' href='https://ap.www.namecheap.com/settings/tools/apiaccess/'>
|
||||
{t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`}
|
||||
</Typography.Link>
|
||||
)
|
||||
case ConnectorProvider.AUTODNS:
|
||||
return <Typography.Link target='_blank' href="https://en.autodns.com/domain-robot-api/">
|
||||
{t`Because of some limitations in API of AutoDNS, we suggest to create an dedicated user for API with limited rights`}
|
||||
</Typography.Link>
|
||||
return (
|
||||
<Typography.Link target='_blank' href='https://en.autodns.com/domain-robot-api/'>
|
||||
{t`Because of some limitations in API of AutoDNS, we suggest to create an dedicated user for API with limited rights`}
|
||||
</Typography.Link>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +51,4 @@ export const tosHyperlink = (provider?: string) => {
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {t} from "ttag";
|
||||
import {regionNames} from "../../i18n";
|
||||
import {t} from 'ttag'
|
||||
import {regionNames} from '../../i18n'
|
||||
|
||||
export const ovhFields = () => ({
|
||||
appKey: t`Application key`,
|
||||
@@ -24,4 +24,4 @@ export const ovhPricingMode = () => [
|
||||
value: 'create-premium',
|
||||
label: t`The domain is free but can be premium. Its price varies from one domain to another`
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
29
eslint.config.mjs
Normal file
29
eslint.config.mjs
Normal 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,
|
||||
];
|
||||
@@ -18,6 +18,7 @@
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@fontsource/noto-color-emoji": "^5.0.27",
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@hotwired/turbo": "^7.1.1 || ^8.0",
|
||||
@@ -37,6 +38,9 @@
|
||||
"axios": "^1.7.2",
|
||||
"core-js": "^3.23.0",
|
||||
"dagre": "^0.8.5",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"globals": "^15.14.0",
|
||||
"html-loader": "^5.1.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"jsonld": "^8.3.2",
|
||||
@@ -51,6 +55,7 @@
|
||||
"ttag": "^1.8.7",
|
||||
"ttag-cli": "^1.10.12",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.19.0",
|
||||
"vcf": "^2.1.2",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
|
||||
Reference in New Issue
Block a user