feat: add eslint linter

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

View File

@@ -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>
</>
)
}

View File

@@ -1,20 +1,17 @@
import {Button, Form, Input, message} from "antd";
import {t} from "ttag";
import React, {useState} from "react";
import {register} from "../utils/api";
import {useNavigate} from "react-router-dom";
import {Button, Form, Input, message} from 'antd'
import {t} from 'ttag'
import React from 'react'
import {register} from '../utils/api'
import {useNavigate} from 'react-router-dom'
import {showErrorAPI} from "../utils/functions/showErrorAPI";
import {showErrorAPI} from '../utils/functions/showErrorAPI'
type FieldType = {
username: string;
password: string;
interface FieldType {
username: string
password: string
}
export function RegisterForm() {
const [error, setError] = useState<string>()
const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
@@ -25,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>
</>
)
}

View File

@@ -1,5 +1,5 @@
import {ItemType} from "antd/lib/menu/interface";
import {t} from "ttag";
import {ItemType} from 'antd/lib/menu/interface'
import {t} from 'ttag'
import {
AimOutlined,
ApiOutlined,
@@ -13,10 +13,10 @@ import {
SearchOutlined,
TableOutlined,
UserOutlined
} from "@ant-design/icons";
import {Menu} from "antd";
import React from "react";
import {useLocation, useNavigate} from "react-router-dom";
} from '@ant-design/icons'
import {Menu} from 'antd'
import React from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
const navigate = useNavigate()
@@ -49,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}
/>
)
}

View File

@@ -1,15 +1,14 @@
import React, {useEffect} from "react";
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {Flex} from "antd";
import {Domain} from "../../utils/api";
import {getLayoutedElements} from "../tracking/watchlist/diagram/getLayoutedElements";
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from "../tracking/watchlist/diagram/watchlistToNodes";
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from "../tracking/watchlist/diagram/watchlistToEdges";
import React, {useEffect} from 'react'
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
import {Flex} from 'antd'
import {Domain} from '../../utils/api'
import {getLayoutedElements} from '../tracking/watchlist/diagram/getLayoutedElements'
import {domainEntitiesToNode, domainToNode, nsToNode, tldToNode} from '../tracking/watchlist/diagram/watchlistToNodes'
import {domainEntitiesToEdges, domainNSToEdges, tldToEdge} from '../tracking/watchlist/diagram/watchlistToEdges'
export function DomainDiagram({domain}: { domain: Domain }) {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
useEffect(() => {
const nodes = [
@@ -33,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>
)
}

View File

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

View File

@@ -1,107 +1,121 @@
import {Badge, Card, Col, Divider, Flex, Row, Space, Tag, Tooltip, Typography} from "antd";
import {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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}}
/>
)
}

View File

@@ -1,10 +1,10 @@
import {Timeline, Tooltip, Typography} from "antd";
import React from "react";
import {Event} from "../../utils/api";
import useBreakpoint from "../../hooks/useBreakpoint";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../utils/functions/rdapTranslation";
import {actionToColor} from "../../utils/functions/actionToColor";
import {actionToIcon} from "../../utils/functions/actionToIcon";
import {Timeline, Tooltip, Typography} from 'antd'
import React from 'react'
import {Event} from '../../utils/api'
import useBreakpoint from '../../hooks/useBreakpoint'
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../utils/functions/rdapTranslation'
import {actionToColor} from '../../utils/functions/actionToColor'
import {actionToIcon} from '../../utils/functions/actionToIcon'
export function EventTimeline({events}: { events: Event[] }) {
const sm = useBreakpoint('sm')
@@ -13,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}&emsp;{dateStr}
</Tooltip>
} : {
label: dateStr,
children: <Tooltip placement='left' title={eventDetail}>{eventName}</Tooltip>,
const text = sm
? {
children: <Tooltip placement='bottom' title={eventDetail}>
{eventName}&emsp;{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
}
}
)
}
/>
</>
}
)}
/>
</>
)
}

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
import React, {useState} from "react";
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
import {t} from "ttag";
import {BankOutlined} from "@ant-design/icons";
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from 'antd'
import React, {useState} from 'react'
import {Connector, ConnectorProvider} from '../../../utils/api/connectors'
import {t} from 'ttag'
import {BankOutlined} from '@ant-design/icons'
import {
ovhEndpointList as ovhEndpointListFunction,
ovhFields as ovhFieldsFunction,
ovhPricingMode as ovhPricingModeFunction,
ovhSubsidiaryList as ovhSubsidiaryListFunction
} from "../../../utils/providers/ovh";
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
} from '../../../utils/providers/ovh'
import {helpGetTokenLink, tosHyperlink} from '../../../utils/providers'
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
sm: {span: 20, offset: 4}
}
}
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
@@ -27,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>
)
}

View File

@@ -1,11 +1,10 @@
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Connector, deleteConnector} from "../../../utils/api/connectors";
const {useToken} = theme;
import {Card, Divider, message, Popconfirm, theme, Typography} from 'antd'
import {t} from 'ttag'
import {DeleteFilled} from '@ant-design/icons'
import React from 'react'
import {Connector, deleteConnector} from '../../../utils/api/connectors'
const {useToken} = theme
export type ConnectorElement = Connector & { id: string, createdAt: string }
@@ -13,28 +12,36 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
const {token} = useToken()
const [messageApi, contextHolder] = message.useMessage()
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
const onConnectorDelete = async (connector: ConnectorElement) => await deleteConnector(connector.id)
.then(onDelete)
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
return <>
{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/>
</>
)}
</>
)
}

View File

@@ -1,22 +1,26 @@
import {CalendarFilled} from "@ant-design/icons";
import {t} from "ttag";
import {Popover, QRCode, Typography} from "antd";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {CalendarFilled} from '@ant-design/icons'
import {t} from 'ttag'
import {Popover, QRCode, Typography} from 'antd'
import React from 'react'
import {Watchlist} from '../../../utils/api'
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
return <Typography.Link href={icsResourceLink}>
<Popover content={<QRCode value={icsResourceLink}
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>
)
}

View File

@@ -1,22 +1,24 @@
import {Popconfirm, theme, Typography} from "antd";
import {t} from "ttag";
import {deleteWatchlist} from "../../../utils/api";
import {DeleteFilled} from "@ant-design/icons";
import React from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {Popconfirm, theme, Typography} from 'antd'
import {t} from 'ttag'
import {deleteWatchlist, Watchlist} from '../../../utils/api'
import {DeleteFilled} from '@ant-design/icons'
import React from 'react'
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
const {token} = theme.useToken()
return <Popconfirm
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>
)
}

View File

@@ -1,27 +1,41 @@
import React, {ReactElement, useEffect, useState} from "react";
import {Domain, getTrackedDomainList} from "../../../utils/api";
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from "antd";
import {t} from "ttag";
import {ColumnType} from "antd/es/table";
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
import {Link} from "react-router-dom";
import React, {ReactElement, useEffect, useState} from 'react'
import {Domain, getTrackedDomainList} from '../../../utils/api'
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from 'antd'
import {t} from 'ttag'
import {ColumnType} from 'antd/es/table'
import {rdapStatusCodeDetailTranslation} from '../../../utils/functions/rdapTranslation'
import {eppStatusCodeToColor} from '../../../utils/functions/eppStatusCodeToColor'
import {Link} from 'react-router-dom'
import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons'
import {DomainToTag} from "../DomainToTag";
import {DomainToTag} from '../DomainToTag'
export function TrackedDomainTable() {
const REDEMPTION_NOTICE = <Tooltip
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}>
<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>
}
</>
)
}

View File

@@ -1,22 +1,20 @@
import {Button, Drawer, Form, Typography} from "antd";
import {t} from "ttag";
import {WatchlistForm} from "./WatchlistForm";
import React, {useState} from "react";
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
import {EditOutlined} from "@ant-design/icons";
import {Connector} from "../../../utils/api/connectors";
import {Button, Drawer, Form, Typography} from 'antd'
import {t} from 'ttag'
import {WatchlistForm} from './WatchlistForm'
import React, {useState} from 'react'
import {EditOutlined} from '@ant-design/icons'
import {Connector} from '../../../utils/api/connectors'
import {Watchlist} from '../../../utils/api'
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
watchlist: Watchlist
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
connectors: Array<Connector & { id: string }>
}) {
const [form] = Form.useForm()
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const showDrawer = () => {
setOpen(true)
}
@@ -26,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>
</>
)
}

View File

@@ -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/>
</>
)
}

View File

@@ -1,49 +1,55 @@
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
import {t} from "ttag";
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
import React from "react";
import {Connector} from "../../../utils/api/connectors";
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
import {actionToColor} from "../../../utils/functions/actionToColor";
import {actionToIcon} from "../../../utils/functions/actionToIcon";
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from 'antd'
import {t} from 'ttag'
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons'
import React from 'react'
import {Connector} from '../../../utils/api/connectors'
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
import {actionToColor} from '../../../utils/functions/actionToColor'
import {actionToIcon} from '../../../utils/functions/actionToIcon'
import {EventAction} from '../../../utils/api'
type TagRender = SelectProps['tagRender'];
type TagRender = SelectProps['tagRender']
const formItemLayout = {
labelCol: {
xs: {span: 24},
sm: {span: 4},
sm: {span: 4}
},
wrapperCol: {
xs: {span: 24},
sm: {span: 20},
},
};
sm: {span: 20}
}
}
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
};
sm: {span: 20, offset: 4}
}
}
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
form: FormInstance,
connectors: (Connector & { id: string })[]
form: FormInstance
connectors: Array<Connector & { id: string }>
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
isCreation: boolean
}) {
const rdapEventNameTranslated = rdapEventNameTranslation()
const rdapEventDetailTranslated = rdapEventDetailTranslation()
const triggerTagRenderer: TagRender = (props) => {
const {value, closable, onClose} = props;
const triggerTagRenderer: TagRender = ({value, closable, onClose}: {
value: EventAction
closable: boolean
onClose: () => void
}) => {
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault()
event.stopPropagation()
}
return (<Tooltip
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}>
return (
<Tooltip
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}
>
<Tag
icon={actionToIcon(value)}
color={actionToColor(value)}
@@ -58,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>
)
}

View File

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

View File

@@ -1,21 +1,20 @@
import {Button, Flex, Modal, Space, Typography} from "antd"
import {t} from "ttag"
import React, {useEffect, useState} from "react"
import {ApartmentOutlined} from "@ant-design/icons"
import {Button, Flex, Modal, Space, Typography} from 'antd'
import {t} from 'ttag'
import React, {useEffect, useState} from 'react'
import {ApartmentOutlined} from '@ant-design/icons'
import '@xyflow/react/dist/style.css'
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {getWatchlist} from "../../../../utils/api";
import {getLayoutedElements} from "./getLayoutedElements";
import {watchlistToNodes} from "./watchlistToNodes";
import {watchlistToEdges} from "./watchlistToEdges";
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
import {getWatchlist} from '../../../../utils/api'
import {getLayoutedElements} from './getLayoutedElements'
import {watchlistToNodes} from './watchlistToNodes'
import {watchlistToEdges} from './watchlistToEdges'
export function ViewDiagramWatchlistButton({token}: { token: string }) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
useEffect(() => {
setEdges([])
@@ -30,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>
</>
)
}

View File

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

View File

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

View File

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