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,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 : [])]
}
}