mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: add eslint linter
This commit is contained in:
@@ -1,21 +1,29 @@
|
||||
import {Tag} from "antd";
|
||||
import {DeleteOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import punycode from "punycode/punycode";
|
||||
import {Link} from "react-router-dom";
|
||||
import React from "react";
|
||||
import {Tag} from 'antd'
|
||||
import {DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
|
||||
import punycode from 'punycode/punycode'
|
||||
import {Link} from 'react-router-dom'
|
||||
import React from 'react'
|
||||
|
||||
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[] } }) {
|
||||
return <Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted ? 'magenta' :
|
||||
domain.status.includes('redemption period') ? 'yellow' :
|
||||
domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted ? <DeleteOutlined/> :
|
||||
domain.status.includes('redemption period') ? <ExclamationCircleOutlined/> :
|
||||
domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}>{punycode.toUnicode(domain.ldhName)}</Tag>
|
||||
</Link>
|
||||
}
|
||||
return (
|
||||
<Link to={'/search/domain/' + domain.ldhName}>
|
||||
<Tag
|
||||
color={
|
||||
domain.deleted
|
||||
? 'magenta'
|
||||
: domain.status.includes('redemption period')
|
||||
? 'yellow'
|
||||
: domain.status.includes('pending delete') ? 'volcano' : 'default'
|
||||
}
|
||||
icon={
|
||||
domain.deleted
|
||||
? <DeleteOutlined/>
|
||||
: domain.status.includes('redemption period')
|
||||
? <ExclamationCircleOutlined/>
|
||||
: domain.status.includes('pending delete') ? <DeleteOutlined/> : null
|
||||
}
|
||||
>{punycode.toUnicode(domain.ldhName)}
|
||||
</Tag>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
17
assets/components/tracking/StatusToTag.tsx
Normal file
17
assets/components/tracking/StatusToTag.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import {Tag, Tooltip} from 'antd'
|
||||
import {eppStatusCodeToColor} from '../../utils/functions/eppStatusCodeToColor'
|
||||
import React from 'react'
|
||||
import {rdapStatusCodeDetailTranslation} from '../../utils/functions/rdapTranslation'
|
||||
|
||||
export function statusToTag(s: string) {
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
|
||||
import {t} from "ttag";
|
||||
import {BankOutlined} from "@ant-design/icons";
|
||||
import {Alert, Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from 'antd'
|
||||
import React, {useState} from 'react'
|
||||
import {Connector, ConnectorProvider} from '../../../utils/api/connectors'
|
||||
import {t} from 'ttag'
|
||||
import {BankOutlined} from '@ant-design/icons'
|
||||
import {
|
||||
ovhEndpointList as ovhEndpointListFunction,
|
||||
ovhFields as ovhFieldsFunction,
|
||||
ovhPricingMode as ovhPricingModeFunction,
|
||||
ovhSubsidiaryList as ovhSubsidiaryListFunction
|
||||
} from "../../../utils/providers/ovh";
|
||||
import {helpGetTokenLink, tosHyperlink} from "../../../utils/providers";
|
||||
} from '../../../utils/providers/ovh'
|
||||
import {helpGetTokenLink, tosHyperlink} from '../../../utils/providers'
|
||||
|
||||
const formItemLayoutWithOutLabel = {
|
||||
wrapperCol: {
|
||||
xs: {span: 24, offset: 0},
|
||||
sm: {span: 20, offset: 4},
|
||||
},
|
||||
sm: {span: 20, offset: 4}
|
||||
}
|
||||
}
|
||||
|
||||
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
|
||||
@@ -27,229 +27,257 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
|
||||
const [open, setOpen] = useState(false)
|
||||
const [ovhPricingModeValue, setOvhPricingModeValue] = useState<string | undefined>()
|
||||
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 14}}
|
||||
onFinish={onCreate}
|
||||
>
|
||||
<Form.Item
|
||||
label={t`Provider`}
|
||||
name="provider"
|
||||
help={helpGetTokenLink(provider)}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
layout='horizontal'
|
||||
labelCol={{span: 6}}
|
||||
wrapperCol={{span: 14}}
|
||||
onFinish={onCreate}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t`Please select a Provider`}
|
||||
suffixIcon={<BankOutlined/>}
|
||||
options={Object.keys(ConnectorProvider).map((c) => ({
|
||||
value: ConnectorProvider[c as keyof typeof ConnectorProvider],
|
||||
label: (
|
||||
<>
|
||||
<BankOutlined/>{" "} {c}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Provider`}
|
||||
name='provider'
|
||||
help={helpGetTokenLink(provider)}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t`Please select a Provider`}
|
||||
suffixIcon={<BankOutlined/>}
|
||||
options={Object.keys(ConnectorProvider).map((c) => ({
|
||||
value: ConnectorProvider[c as keyof typeof ConnectorProvider],
|
||||
label: (
|
||||
<>
|
||||
<BankOutlined/>{' '} {c}
|
||||
</>
|
||||
)
|
||||
}))}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{
|
||||
provider === ConnectorProvider.OVH && <>
|
||||
{
|
||||
Object.keys(ovhFields).map(fieldName => <Form.Item
|
||||
label={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
name={['authData', fieldName]}
|
||||
{
|
||||
provider === ConnectorProvider.OVH && <>
|
||||
{
|
||||
Object.keys(ovhFields).map(fieldName => <Form.Item
|
||||
key={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
label={ovhFields[fieldName as keyof typeof ovhFields]}
|
||||
name={['authData', fieldName]}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>)
|
||||
}
|
||||
<Form.Item
|
||||
label={t`OVH Endpoint`}
|
||||
name={['authData', 'apiEndpoint']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>)
|
||||
}
|
||||
<Form.Item
|
||||
label={t`OVH Endpoint`}
|
||||
name={['authData', 'apiEndpoint']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Select options={ovhEndpointList} optionFilterProp="label"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`OVH subsidiary`}
|
||||
name={['authData', 'ovhSubsidiary']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Select options={ovhSubsidiaryList} optionFilterProp="label"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`OVH pricing mode`}
|
||||
name={['authData', 'pricingMode']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Popconfirm
|
||||
title={t`Confirm pricing mode`}
|
||||
description={t`Are you sure about this setting? This may result in additional charges from the API Provider`}
|
||||
onCancel={() => {
|
||||
form.resetFields(['authData'])
|
||||
setOvhPricingModeValue(undefined)
|
||||
setOpen(false)
|
||||
}}
|
||||
onConfirm={() => setOpen(false)}
|
||||
open={open}
|
||||
<Select options={ovhEndpointList} optionFilterProp='label'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`OVH subsidiary`}
|
||||
name={['authData', 'ovhSubsidiary']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Select options={ovhPricingMode} optionFilterProp="label" value={ovhPricingModeValue}
|
||||
<Select options={ovhSubsidiaryList} optionFilterProp='label'/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`OVH pricing mode`}
|
||||
name={['authData', 'pricingMode']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Popconfirm
|
||||
title={t`Confirm pricing mode`}
|
||||
description={t`Are you sure about this setting? This may result in additional charges from the API Provider`}
|
||||
onCancel={() => {
|
||||
form.resetFields(['authData'])
|
||||
setOvhPricingModeValue(undefined)
|
||||
setOpen(false)
|
||||
}}
|
||||
onConfirm={() => setOpen(false)}
|
||||
open={open}
|
||||
>
|
||||
<Select
|
||||
options={ovhPricingMode} optionFilterProp='label' value={ovhPricingModeValue}
|
||||
onChange={(value: string) => {
|
||||
setOvhPricingModeValue(value)
|
||||
form.setFieldValue(['authData', 'pricingMode'], value)
|
||||
if (value !== 'create-default') {
|
||||
setOpen(true)
|
||||
}
|
||||
}}/>
|
||||
</Popconfirm>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.GANDI && <>
|
||||
<Form.Item
|
||||
label={t`Personal Access Token (PAT)`}
|
||||
name={['authData', 'token']}
|
||||
rules={[{required: true, message: t`Required`}]}>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Organization sharing ID`}
|
||||
name={['authData', 'sharingId']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`It indicates the organization that will pay for the ordered product`}</Typography.Text>}
|
||||
required={false}>
|
||||
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.AUTODNS && <>
|
||||
<Alert
|
||||
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
|
||||
type="warning"/>
|
||||
<br/>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Username`}
|
||||
name={['authData', 'username']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}>
|
||||
<Input autoComplete='off' required={true}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Password`}
|
||||
name={['authData', 'password']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required={true}>
|
||||
<Input.Password autoComplete='off' required={true} placeholder=''/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Owner nic-handle`}
|
||||
name={['authData', 'contactid']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`The nic-handle of the domain name owner`}<a
|
||||
href="https://cloud.autodns.com/contacts/domain">{t`You can get it from this page`}</a></Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required={true}>
|
||||
<Input autoComplete='off' required={true} placeholder=''/>
|
||||
</Form.Item>
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.GANDI && <>
|
||||
<Form.Item
|
||||
label={t`Personal Access Token (PAT)`}
|
||||
name={['authData', 'token']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Organization sharing ID`}
|
||||
name={['authData', 'sharingId']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`It indicates the organization that will pay for the ordered product`}
|
||||
</Typography.Text>}
|
||||
required={false}
|
||||
>
|
||||
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.AUTODNS && <>
|
||||
<Alert
|
||||
message={t`This provider does not provide a list of supported TLD. Please double check if the domain you want to register is supported.`}
|
||||
type='warning'
|
||||
/>
|
||||
<br/>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Username`}
|
||||
name={['authData', 'username']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`Attention: AutoDNS do not support 2-Factor Authentication on API Users for automated systems`}
|
||||
</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Input autoComplete='off' required/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`AutoDNS Password`}
|
||||
name={['authData', 'password']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required
|
||||
>
|
||||
<Input.Password autoComplete='off' required placeholder=''/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`Owner nic-handle`}
|
||||
name={['authData', 'contactid']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`The nic-handle of the domain name owner`}<a
|
||||
href='https://cloud.autodns.com/contacts/domain'
|
||||
>{t`You can get it from this page`}
|
||||
</a>
|
||||
</Typography.Text>}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
required
|
||||
>
|
||||
<Input autoComplete='off' required placeholder=''/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`Context Value`}
|
||||
name={['authData', 'context']}
|
||||
help={<Typography.Text
|
||||
type='secondary'>{t`If you not sure, use the default value 4`}</Typography.Text>}
|
||||
<Form.Item
|
||||
label={t`Context Value`}
|
||||
name={['authData', 'context']}
|
||||
help={<Typography.Text
|
||||
type='secondary'
|
||||
>{t`If you not sure, use the default value 4`}
|
||||
</Typography.Text>}
|
||||
|
||||
required={false}>
|
||||
<Input autoComplete='off' required={false} placeholder='4'/>
|
||||
</Form.Item>
|
||||
required={false}
|
||||
>
|
||||
<Input autoComplete='off' required={false} placeholder='4'/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Owner confirmation`}
|
||||
name={['authData', 'ownerConfirm']}
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Owner confirmation`}
|
||||
name={['authData', 'ownerConfirm']}
|
||||
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`Owner confirms his consent of domain order jobs`}</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`Owner confirms his consent of domain order jobs`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.NAMECHEAP && <>
|
||||
<Form.Item
|
||||
label={t`Username`}
|
||||
name={['authData', 'ApiUser']}
|
||||
>
|
||||
<Input autoComplete='off'></Input>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`API key`}
|
||||
name={['authData', 'ApiKey']}
|
||||
>
|
||||
<Input autoComplete='off'></Input>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
}
|
||||
{
|
||||
provider === ConnectorProvider.NAMECHEAP && <>
|
||||
<Form.Item
|
||||
label={t`Username`}
|
||||
name={['authData', 'ApiUser']}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t`API key`}
|
||||
name={['authData', 'ApiKey']}
|
||||
>
|
||||
<Input autoComplete='off'/>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
provider !== undefined && <>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`API Terms of Service`}
|
||||
name={['authData', 'acceptConditions']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
style={{marginTop: '3em'}}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>
|
||||
<Typography.Link target='_blank' href={tosHyperlink(provider)}>
|
||||
{t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
|
||||
</Typography.Link>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`Legal age`}
|
||||
name={['authData', 'ownerLegalAge']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`I am of the minimum age required to consent to these conditions`}</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t`Withdrawal period`}
|
||||
name={['authData', 'waiveRetractationPeriod']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required={true}>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
{
|
||||
provider !== undefined && <>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`API Terms of Service`}
|
||||
name={['authData', 'acceptConditions']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
style={{marginTop: '3em'}}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>
|
||||
<Typography.Link target='_blank' href={tosHyperlink(provider)}>
|
||||
{t`I have read and accepted the conditions of use of the Provider API, accessible from this hyperlink`}
|
||||
</Typography.Link>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Legal age`}
|
||||
name={['authData', 'ownerLegalAge']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`I am of the minimum age required to consent to these conditions`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
valuePropName='checked'
|
||||
label={t`Withdrawal period`}
|
||||
name={['authData', 'waiveRetractationPeriod']}
|
||||
rules={[{required: true, message: t`Required`}]}
|
||||
>
|
||||
<Checkbox
|
||||
required
|
||||
>{t`I waive my right of withdrawal regarding the purchase of domain names via the Provider's API`}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t`Create`}
|
||||
</Button>
|
||||
<Button type='default' htmlType='reset'>
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {Card, Divider, message, Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector, deleteConnector} from "../../../utils/api/connectors";
|
||||
|
||||
const {useToken} = theme;
|
||||
import {Card, Divider, message, Popconfirm, theme, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {DeleteFilled} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import {Connector, deleteConnector} from '../../../utils/api/connectors'
|
||||
|
||||
const {useToken} = theme
|
||||
|
||||
export type ConnectorElement = Connector & { id: string, createdAt: string }
|
||||
|
||||
@@ -13,28 +12,36 @@ export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorEl
|
||||
const {token} = useToken()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const onConnectorDelete = (connector: ConnectorElement) => deleteConnector(connector.id)
|
||||
const onConnectorDelete = async (connector: ConnectorElement) => await deleteConnector(connector.id)
|
||||
.then(onDelete)
|
||||
.catch(() => messageApi.error(t`An error occurred while deleting the Connector. Make sure it is not used in any Watchlist`))
|
||||
|
||||
return <>
|
||||
{connectors.map(connector =>
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card hoverable title={<Typography.Text
|
||||
title={new Date(connector.createdAt).toLocaleString()}>{t`Connector ${connector.provider}`}</Typography.Text>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Popconfirm title={t`Delete the Connector`}
|
||||
description={t`Are you sure to delete this Connector?`}
|
||||
onConfirm={() => onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>
|
||||
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{connectors.map(connector =>
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card
|
||||
hoverable title={<Typography.Text
|
||||
title={new Date(connector.createdAt).toLocaleString()}
|
||||
>{t`Connector ${connector.provider}`}
|
||||
</Typography.Text>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Popconfirm
|
||||
title={t`Delete the Connector`}
|
||||
description={t`Are you sure to delete this Connector?`}
|
||||
onConfirm={async () => await onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/>
|
||||
</Popconfirm>}
|
||||
>
|
||||
<Card.Meta description={connector.id} style={{marginBottom: '1em'}}/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import {CalendarFilled} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {Popover, QRCode, Typography} from "antd";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {CalendarFilled} from '@ant-design/icons'
|
||||
import {t} from 'ttag'
|
||||
import {Popover, QRCode, Typography} from 'antd'
|
||||
import React from 'react'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function CalendarWatchlistButton({watchlist}: { watchlist: Watchlist }) {
|
||||
|
||||
const icsResourceLink = `${window.location.origin}/api/watchlists/${watchlist.token}/calendar`
|
||||
|
||||
return <Typography.Link href={icsResourceLink}>
|
||||
<Popover content={<QRCode value={icsResourceLink}
|
||||
bordered={false}
|
||||
title={t`QR Code for iCalendar export`}
|
||||
type='svg'
|
||||
/>}>
|
||||
<CalendarFilled title={t`Export events to iCalendar format`}
|
||||
style={{color: 'limegreen'}}
|
||||
/>
|
||||
</Popover>
|
||||
</Typography.Link>
|
||||
}
|
||||
return (
|
||||
<Typography.Link href={icsResourceLink}>
|
||||
<Popover content={<QRCode
|
||||
value={icsResourceLink}
|
||||
bordered={false}
|
||||
title={t`QR Code for iCalendar export`}
|
||||
type='svg'
|
||||
/>}
|
||||
>
|
||||
<CalendarFilled
|
||||
title={t`Export events to iCalendar format`}
|
||||
style={{color: 'limegreen'}}
|
||||
/>
|
||||
</Popover>
|
||||
</Typography.Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import {Popconfirm, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {deleteWatchlist} from "../../../utils/api";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Popconfirm, theme, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {deleteWatchlist, Watchlist} from '../../../utils/api'
|
||||
import {DeleteFilled} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
export function DeleteWatchlistButton({watchlist, onDelete}: { watchlist: Watchlist, onDelete: () => void }) {
|
||||
const {token} = theme.useToken()
|
||||
|
||||
return <Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={() => deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}>
|
||||
<Typography.Link>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm>
|
||||
}
|
||||
return (
|
||||
<Popconfirm
|
||||
title={t`Delete the Watchlist`}
|
||||
description={t`Are you sure to delete this Watchlist?`}
|
||||
onConfirm={async () => await deleteWatchlist(watchlist.token).then(onDelete)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
okButtonProps={{danger: true}}
|
||||
>
|
||||
<Typography.Link>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Typography.Link>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import {Domain, getTrackedDomainList} from "../../../utils/api";
|
||||
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
|
||||
import {Link} from "react-router-dom";
|
||||
import React, {ReactElement, useEffect, useState} from 'react'
|
||||
import {Domain, getTrackedDomainList} from '../../../utils/api'
|
||||
import {Button, Empty, Result, Skeleton, Table, Tag, Tooltip} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {ColumnType} from 'antd/es/table'
|
||||
import {rdapStatusCodeDetailTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {eppStatusCodeToColor} from '../../../utils/functions/eppStatusCodeToColor'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {ExceptionOutlined, MonitorOutlined} from '@ant-design/icons'
|
||||
import {DomainToTag} from "../DomainToTag";
|
||||
|
||||
import {DomainToTag} from '../DomainToTag'
|
||||
|
||||
export function TrackedDomainTable() {
|
||||
const REDEMPTION_NOTICE = <Tooltip
|
||||
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}>
|
||||
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
|
||||
</Tooltip>
|
||||
const REDEMPTION_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is in redemption period and will potentially be deleted soon`}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('redemption period')}>redemption period</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const PENDING_DELETE_NOTICE = <Tooltip
|
||||
title={t`At least one domain name is pending deletion and will soon become available for registration again`}>
|
||||
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
|
||||
</Tooltip>
|
||||
const PENDING_DELETE_NOTICE = (
|
||||
<Tooltip
|
||||
title={t`At least one domain name is pending deletion and will soon become available for registration again`}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor('pending delete')}>pending delete</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const [dataTable, setDataTable] = useState<(Domain & { domain: Domain })[]>([])
|
||||
interface TableRow {
|
||||
key: string
|
||||
ldhName: ReactElement
|
||||
expirationDate: string
|
||||
status: ReactElement[]
|
||||
updatedAt: string
|
||||
domain: Domain
|
||||
}
|
||||
|
||||
const [dataTable, setDataTable] = useState<TableRow[]>([])
|
||||
const [total, setTotal] = useState<number>()
|
||||
const [specialNotice, setSpecialNotice] = useState<ReactElement[]>([])
|
||||
|
||||
@@ -46,8 +60,10 @@ export function TrackedDomainTable() {
|
||||
ldhName: <DomainToTag domain={d}/>,
|
||||
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
|
||||
status: d.status.map(s => <Tooltip
|
||||
key={s}
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -63,17 +79,19 @@ export function TrackedDomainTable() {
|
||||
fetchData({page: 1, itemsPerPage: 30})
|
||||
}, [])
|
||||
|
||||
interface RecordType {
|
||||
domain: Domain
|
||||
}
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
const columns: Array<ColumnType<RecordType>> = [
|
||||
{
|
||||
title: t`Domain`,
|
||||
dataIndex: "ldhName"
|
||||
dataIndex: 'ldhName'
|
||||
},
|
||||
{
|
||||
title: t`Expiration date`,
|
||||
dataIndex: 'expirationDate',
|
||||
sorter: (a: { domain: Domain }, b: { domain: Domain }) => {
|
||||
|
||||
sorter: (a: RecordType, b: RecordType) => {
|
||||
const expirationDate1 = a.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
const expirationDate2 = b.domain.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
|
||||
@@ -85,65 +103,70 @@ export function TrackedDomainTable() {
|
||||
{
|
||||
title: t`Updated at`,
|
||||
dataIndex: 'updatedAt',
|
||||
sorter: (a: { domain: Domain }, b: {
|
||||
domain: Domain
|
||||
}) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
|
||||
sorter: (a: RecordType, b: RecordType) => new Date(a.domain.updatedAt).getTime() - new Date(b.domain.updatedAt).getTime()
|
||||
},
|
||||
{
|
||||
title: t`Status`,
|
||||
dataIndex: 'status',
|
||||
showSorterTooltip: {target: 'full-header'},
|
||||
filters: [...new Set(dataTable.map((d: any) => d.domain.status).flat())].map(s => ({
|
||||
filters: [...new Set(dataTable.map((d: RecordType) => d.domain.status).flat())].map(s => ({
|
||||
text: <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}>
|
||||
title={rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>,
|
||||
value: s,
|
||||
value: s
|
||||
})),
|
||||
onFilter: (value, record: { domain: Domain }) => record.domain.status.includes(value as string)
|
||||
onFilter: (value, record: RecordType) => record.domain.status.includes(value as string)
|
||||
}
|
||||
]
|
||||
|
||||
return <>
|
||||
{
|
||||
total === 0 ? <Empty
|
||||
description={t`No tracked domain names were found, please create your first Watchlist`}
|
||||
>
|
||||
<Link to='/tracking/watchlist'>
|
||||
<Button type="primary">Create Now</Button>
|
||||
</Link>
|
||||
</Empty> : <Skeleton loading={total === undefined}>
|
||||
<Result
|
||||
style={{paddingTop: 0}}
|
||||
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
|
||||
{...(specialNotice.length > 0 ? {
|
||||
icon: <ExceptionOutlined/>,
|
||||
status: 'warning',
|
||||
title: t`At least one domain name you are tracking requires special attention`,
|
||||
extra: specialNotice
|
||||
} : {
|
||||
icon: <MonitorOutlined/>,
|
||||
status: 'info',
|
||||
title: t`The domain names below are subject to special monitoring`,
|
||||
})}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
{
|
||||
total === 0
|
||||
? <Empty
|
||||
description={t`No tracked domain names were found, please create your first Watchlist`}
|
||||
>
|
||||
<Link to='/tracking/watchlist'>
|
||||
<Button type='primary'>Create Now</Button>
|
||||
</Link>
|
||||
</Empty>
|
||||
: <Skeleton loading={total === undefined}>
|
||||
<Result
|
||||
style={{paddingTop: 0}}
|
||||
subTitle={t`Please note that this table does not include domain names marked as expired or those with an unknown expiration date`}
|
||||
{...(specialNotice.length > 0
|
||||
? {
|
||||
icon: <ExceptionOutlined/>,
|
||||
status: 'warning',
|
||||
title: t`At least one domain name you are tracking requires special attention`,
|
||||
extra: specialNotice
|
||||
}
|
||||
: {
|
||||
icon: <MonitorOutlined/>,
|
||||
status: 'info',
|
||||
title: t`The domain names below are subject to special monitoring`
|
||||
})}
|
||||
/>
|
||||
|
||||
<Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
scroll={{y: '50vh'}}
|
||||
/>
|
||||
</Skeleton>
|
||||
}
|
||||
</>
|
||||
}
|
||||
<Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
scroll={{y: '50vh'}}
|
||||
/>
|
||||
</Skeleton>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import {Button, Drawer, Form, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {WatchlistForm} from "./WatchlistForm";
|
||||
import React, {useState} from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {Button, Drawer, Form, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {WatchlistForm} from './WatchlistForm'
|
||||
import React, {useState} from 'react'
|
||||
import {EditOutlined} from '@ant-design/icons'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
|
||||
const showDrawer = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
@@ -26,43 +24,46 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<EditOutlined title={t`Edit the Watchlist`} onClick={() => {
|
||||
showDrawer()
|
||||
form.setFields([
|
||||
{name: 'token', value: watchlist.token},
|
||||
{name: 'name', value: watchlist.name},
|
||||
{name: 'connector', value: watchlist.connector?.id},
|
||||
{name: 'domains', value: watchlist.domains.map(d => d.ldhName)},
|
||||
{name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
|
||||
{name: 'dsn', value: watchlist.dsn}
|
||||
])
|
||||
}}/>
|
||||
</Typography.Link>
|
||||
<Drawer
|
||||
title={t`Update a Watchlist`}
|
||||
width='80%'
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
loading={loading}
|
||||
styles={{
|
||||
body: {
|
||||
paddingBottom: 80,
|
||||
}
|
||||
}}
|
||||
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
|
||||
>
|
||||
<WatchlistForm
|
||||
form={form}
|
||||
onFinish={values => {
|
||||
setLoading(true)
|
||||
onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false))
|
||||
return (
|
||||
<>
|
||||
<Typography.Link>
|
||||
<EditOutlined
|
||||
title={t`Edit the Watchlist`} onClick={() => {
|
||||
showDrawer()
|
||||
form.setFields([
|
||||
{name: 'token', value: watchlist.token},
|
||||
{name: 'name', value: watchlist.name},
|
||||
{name: 'connector', value: watchlist.connector?.id},
|
||||
{name: 'domains', value: watchlist.domains.map(d => d.ldhName)},
|
||||
{name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
|
||||
{name: 'dsn', value: watchlist.dsn}
|
||||
])
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
}
|
||||
/>
|
||||
</Typography.Link>
|
||||
<Drawer
|
||||
title={t`Update a Watchlist`}
|
||||
width='80%'
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
loading={loading}
|
||||
styles={{
|
||||
body: {
|
||||
paddingBottom: 80
|
||||
}
|
||||
}}
|
||||
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
|
||||
>
|
||||
<WatchlistForm
|
||||
form={form}
|
||||
onFinish={values => {
|
||||
setLoading(true)
|
||||
onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false))
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,83 @@
|
||||
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from "antd";
|
||||
import {DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import {t} from "ttag";
|
||||
import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
|
||||
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
|
||||
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint";
|
||||
import {CalendarWatchlistButton} from "./CalendarWatchlistButton";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from 'antd'
|
||||
import {DisconnectOutlined, LinkOutlined} from '@ant-design/icons'
|
||||
import {t} from 'ttag'
|
||||
import {ViewDiagramWatchlistButton} from './diagram/ViewDiagramWatchlistButton'
|
||||
import {UpdateWatchlistButton} from './UpdateWatchlistButton'
|
||||
import {DeleteWatchlistButton} from './DeleteWatchlistButton'
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {CalendarWatchlistButton} from './CalendarWatchlistButton'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
import {DomainToTag} from "../DomainToTag";
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {DomainToTag} from '../DomainToTag'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[],
|
||||
watchlist: Watchlist
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
return <>
|
||||
<Card
|
||||
type='inner'
|
||||
title={<>
|
||||
{
|
||||
watchlist.connector ?
|
||||
<Tooltip title={watchlist.connector.id}>
|
||||
<Tag icon={<LinkOutlined/>} color="lime-inverse"/>
|
||||
</Tooltip> :
|
||||
<Tooltip title={t`This Watchlist is not linked to a Connector.`}>
|
||||
<Tag icon={<DisconnectOutlined/>} color="default"/>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip title={new Date(watchlist.createdAt).toLocaleString()}>
|
||||
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={
|
||||
<Space size='middle'>
|
||||
<ViewDiagramWatchlistButton token={watchlist.token}/>
|
||||
|
||||
<CalendarWatchlistButton watchlist={watchlist}/>
|
||||
|
||||
<UpdateWatchlistButton
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
/>
|
||||
|
||||
<DeleteWatchlistButton watchlist={watchlist} onDelete={onDelete}/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
{watchlist.domains.map(d => <DomainToTag domain={d}/>)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
{watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tooltip
|
||||
title={rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] || undefined}>
|
||||
<Tag color={actionToColor(t.event)}>
|
||||
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
type='inner'
|
||||
title={<>
|
||||
{
|
||||
(watchlist.connector != null)
|
||||
? <Tooltip title={watchlist.connector.id}>
|
||||
<Tag icon={<LinkOutlined/>} color='lime-inverse'/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
}
|
||||
: <Tooltip title={t`This Watchlist is not linked to a Connector.`}>
|
||||
<Tag icon={<DisconnectOutlined/>} color='default'/>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip title={new Date(watchlist.createdAt).toLocaleString()}>
|
||||
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
|
||||
</Tooltip>
|
||||
</>}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={
|
||||
<Space size='middle'>
|
||||
<ViewDiagramWatchlistButton token={watchlist.token}/>
|
||||
|
||||
<CalendarWatchlistButton watchlist={watchlist}/>
|
||||
|
||||
<UpdateWatchlistButton
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
/>
|
||||
|
||||
<DeleteWatchlistButton watchlist={watchlist} onDelete={onDelete}/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
{watchlist.domains.map(d => <DomainToTag key={d.ldhName} domain={d}/>)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
{watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tooltip
|
||||
key={t.event}
|
||||
title={rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag color={actionToColor(t.event)}>
|
||||
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,55 @@
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
import {actionToIcon} from "../../../utils/functions/actionToIcon";
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Tooltip, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation'
|
||||
import {actionToColor} from '../../../utils/functions/actionToColor'
|
||||
import {actionToIcon} from '../../../utils/functions/actionToIcon'
|
||||
import {EventAction} from '../../../utils/api'
|
||||
|
||||
type TagRender = SelectProps['tagRender'];
|
||||
type TagRender = SelectProps['tagRender']
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
sm: {span: 4}
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 20},
|
||||
},
|
||||
};
|
||||
sm: {span: 20}
|
||||
}
|
||||
}
|
||||
|
||||
const formItemLayoutWithOutLabel = {
|
||||
wrapperCol: {
|
||||
xs: {span: 24, offset: 0},
|
||||
sm: {span: 20, offset: 4},
|
||||
},
|
||||
};
|
||||
sm: {span: 20, offset: 4}
|
||||
}
|
||||
}
|
||||
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
form: FormInstance,
|
||||
connectors: (Connector & { id: string })[]
|
||||
form: FormInstance
|
||||
connectors: Array<Connector & { id: string }>
|
||||
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
|
||||
isCreation: boolean
|
||||
}) {
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const triggerTagRenderer: TagRender = (props) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const triggerTagRenderer: TagRender = ({value, closable, onClose}: {
|
||||
value: EventAction
|
||||
closable: boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return (<Tooltip
|
||||
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}>
|
||||
return (
|
||||
<Tooltip
|
||||
title={rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] || undefined}
|
||||
>
|
||||
<Tag
|
||||
icon={actionToIcon(value)}
|
||||
color={actionToColor(value)}
|
||||
@@ -58,203 +64,220 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
)
|
||||
}
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
>
|
||||
|
||||
<Form.Item name='token' hidden>
|
||||
<Input hidden/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t`Name`}
|
||||
name='name'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20},
|
||||
}}
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
>
|
||||
<Input placeholder={t`Watchlist Name`}
|
||||
title={t`Naming the Watchlist makes it easier to find in the list below.`}
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name="domains"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, domains) => {
|
||||
if (!domains || domains.length < 1) {
|
||||
return Promise.reject(new Error(t`At least one domain name`));
|
||||
|
||||
<Form.Item name='token' hidden>
|
||||
<Input hidden/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t`Name`}
|
||||
name='name'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20}
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder={t`Watchlist Name`}
|
||||
title={t`Naming the Watchlist makes it easier to find in the list below.`}
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name='domains'
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, domains) => {
|
||||
if (!domains || domains.length < 1) {
|
||||
return await Promise.reject(new Error(t`At least one domain name`))
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`Domain names` : ''}
|
||||
required={true}
|
||||
key={field.key}
|
||||
>
|
||||
}
|
||||
]}
|
||||
>
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /^(?=.*\.)\S*[^.\s]$/,
|
||||
message: t`This domain name does not appear to be valid`,
|
||||
max: 63,
|
||||
min: 2
|
||||
}]}
|
||||
noStyle
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`Domain names` : ''}
|
||||
required
|
||||
key={field.key}
|
||||
>
|
||||
<Input placeholder={t`Domain name`} style={{width: '60%'}} autoComplete='off'/>
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /^(?=.*\.)\S*[^.\s]$/,
|
||||
message: t`This domain name does not appear to be valid`,
|
||||
max: 63,
|
||||
min: 2
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t`Domain name`} style={{width: '60%'}} autoComplete='off'/>
|
||||
</Form.Item>
|
||||
{fields.length > 1
|
||||
? (
|
||||
<MinusCircleOutlined
|
||||
className='dynamic-delete-button'
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type='dashed'
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Domain name`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Domain name`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item label={t`Tracked events`}
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20},
|
||||
}}
|
||||
required
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
tagRender={triggerTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
options={Object.keys(rdapEventNameTranslated).map(e => ({
|
||||
value: e,
|
||||
title: rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] || undefined,
|
||||
label: rdapEventNameTranslated[e as keyof typeof rdapEventNameTranslated]
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item
|
||||
label={t`Tracked events`}
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<Select
|
||||
mode='multiple'
|
||||
tagRender={triggerTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
options={Object.keys(rdapEventNameTranslated).map(e => ({
|
||||
value: e,
|
||||
title: rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] || undefined,
|
||||
label: rdapEventNameTranslated[e as keyof typeof rdapEventNameTranslated]
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t`Connector`}
|
||||
name='connector'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4},
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20},
|
||||
}}
|
||||
help={t`Please make sure the connector information is valid to purchase a domain that may be available soon.`}
|
||||
>
|
||||
<Select showSearch
|
||||
<Form.Item
|
||||
label={t`Connector`}
|
||||
name='connector'
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
sm: {span: 4}
|
||||
}}
|
||||
wrapperCol={{
|
||||
md: {span: 12},
|
||||
sm: {span: 20}
|
||||
}}
|
||||
help={t`Please make sure the connector information is valid to purchase a domain that may be available soon.`}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={t`Connector`}
|
||||
suffixIcon={<ApiOutlined/>}
|
||||
optionFilterProp="label"
|
||||
optionFilterProp='label'
|
||||
options={connectors.map(c => ({
|
||||
label: `${c.provider} (${c.id})`,
|
||||
value: c.id
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name="dsn">
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`DSN` : ''}
|
||||
required={true}
|
||||
key={field.key}
|
||||
>
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.List
|
||||
name='dsn'
|
||||
>
|
||||
{(fields, {add, remove}, {errors}) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /:\/\//,
|
||||
message: t`This DSN does not appear to be valid`
|
||||
}]}
|
||||
noStyle
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={index === 0 ? t`DSN` : ''}
|
||||
required
|
||||
key={field.key}
|
||||
>
|
||||
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
|
||||
autoComplete='off'/>
|
||||
<Form.Item
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /:\/\//,
|
||||
message: t`This DSN does not appear to be valid`
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder='slack://TOKEN@default?channel=CHANNEL' style={{width: '60%'}}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</Form.Item>
|
||||
{fields.length > 0
|
||||
? (
|
||||
<MinusCircleOutlined
|
||||
className='dynamic-delete-button'
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Form.Item>
|
||||
{fields.length > 0 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item help={
|
||||
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'
|
||||
target='_blank'>
|
||||
{t`Check out this link to the Symfony documentation to help you build the DSN`}
|
||||
</Typography.Link>}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
))}
|
||||
<Form.Item help={
|
||||
<Typography.Link
|
||||
href='https://symfony.com/doc/current/notifier.html#chat-channel'
|
||||
target='_blank'
|
||||
>
|
||||
{t`Check out this link to the Symfony documentation to help you build the DSN`}
|
||||
</Typography.Link>
|
||||
}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
<Button
|
||||
type='dashed'
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type='default' htmlType='reset'>
|
||||
{t`Reset`}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {WatchlistCard} from "./WatchlistCard";
|
||||
import React from 'react'
|
||||
import {Connector} from '../../../utils/api/connectors'
|
||||
import {WatchlistCard} from './WatchlistCard'
|
||||
import {Watchlist} from '../../../utils/api'
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
|
||||
watchlists: Watchlist[],
|
||||
onDelete: () => void,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
watchlists: Watchlist[]
|
||||
onDelete: () => void
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>
|
||||
connectors: Array<Connector & { id: string }>
|
||||
}) {
|
||||
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard
|
||||
key={watchlist.token}
|
||||
watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {Button, Flex, Modal, Space, Typography} from "antd"
|
||||
import {t} from "ttag"
|
||||
import React, {useEffect, useState} from "react"
|
||||
import {ApartmentOutlined} from "@ant-design/icons"
|
||||
import {Button, Flex, Modal, Space, Typography} from 'antd'
|
||||
import {t} from 'ttag'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {ApartmentOutlined} from '@ant-design/icons'
|
||||
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
|
||||
import {getWatchlist} from "../../../../utils/api";
|
||||
import {getLayoutedElements} from "./getLayoutedElements";
|
||||
import {watchlistToNodes} from "./watchlistToNodes";
|
||||
import {watchlistToEdges} from "./watchlistToEdges";
|
||||
import {Background, Controls, Edge, MiniMap, Node, ReactFlow, useEdgesState, useNodesState} from '@xyflow/react'
|
||||
import {getWatchlist} from '../../../../utils/api'
|
||||
import {getLayoutedElements} from './getLayoutedElements'
|
||||
import {watchlistToNodes} from './watchlistToNodes'
|
||||
import {watchlistToEdges} from './watchlistToEdges'
|
||||
|
||||
export function ViewDiagramWatchlistButton({token}: { token: string }) {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
|
||||
useEffect(() => {
|
||||
setEdges([])
|
||||
@@ -30,52 +29,54 @@ export function ViewDiagramWatchlistButton({token}: { token: string }) {
|
||||
setNodes(e.nodes)
|
||||
setEdges(e.edges)
|
||||
}).catch(() => setOpen(false)).finally(() => setLoading(false))
|
||||
|
||||
}, [open])
|
||||
|
||||
|
||||
return <>
|
||||
<Typography.Link>
|
||||
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
|
||||
style={{color: 'darkviolet'}}
|
||||
onClick={() => setOpen(true)}/>
|
||||
</Typography.Link>
|
||||
<Modal
|
||||
title={t`Watchlist Entity Diagram`}
|
||||
centered
|
||||
open={open}
|
||||
loading={loading}
|
||||
footer={
|
||||
<Space>
|
||||
<Button type="default" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onOk={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
width='90vw'
|
||||
height='100%'
|
||||
>
|
||||
<Flex style={{width: '85vw', height: '85vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
defaultEdges={[]}
|
||||
defaultNodes={[]}
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<Typography.Link>
|
||||
<ApartmentOutlined
|
||||
title={t`View the Watchlist Entity Diagram`}
|
||||
style={{color: 'darkviolet'}}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</Typography.Link>
|
||||
<Modal
|
||||
title={t`Watchlist Entity Diagram`}
|
||||
centered
|
||||
open={open}
|
||||
loading={loading}
|
||||
footer={
|
||||
<Space>
|
||||
<Button type='default' onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onOk={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
width='90vw'
|
||||
height='100%'
|
||||
>
|
||||
<Flex style={{width: '85vw', height: '85vh'}}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
colorMode='dark'
|
||||
defaultEdges={[]}
|
||||
defaultNodes={[]}
|
||||
nodesConnectable={false}
|
||||
edgesReconnectable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
>
|
||||
<MiniMap/>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import dagre from "dagre"
|
||||
import dagre from 'dagre'
|
||||
import {Edge, Node, Position} from '@xyflow/react'
|
||||
|
||||
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
|
||||
export const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const nodeWidth = 172
|
||||
const nodeHeight = 200
|
||||
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({rankdir: direction});
|
||||
const isHorizontal = direction === 'LR'
|
||||
dagreGraph.setGraph({rankdir: direction})
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
|
||||
});
|
||||
nodes.forEach(node => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight})
|
||||
})
|
||||
|
||||
edges.forEach((edge: any) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
edges.forEach(edge => {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
})
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
const newNodes = nodes.map((node: any) => {
|
||||
const newNodes: Node[] = nodes.map(node => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? 'left' : 'top',
|
||||
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {nodes: newNodes, edges};
|
||||
}
|
||||
return {nodes: newNodes, edges}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import {Domain, Watchlist} from "../../../../utils/api";
|
||||
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
|
||||
import {t} from "ttag";
|
||||
import {Domain, Watchlist} from '../../../../utils/api'
|
||||
import {rdapRoleTranslation} from '../../../../utils/functions/rdapTranslation'
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
|
||||
import {rolesToColor} from '../../../../utils/functions/rolesToColor'
|
||||
import {Edge} from '@xyflow/react'
|
||||
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false): Edge[] {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
.map(e => ({
|
||||
id: `e-${d.ldhName}-${e.entity.handle}`,
|
||||
@@ -21,11 +22,11 @@ export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
label: e.roles
|
||||
.map(r => rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] || r)
|
||||
.join(', '),
|
||||
animated: e.roles.includes('registrant'),
|
||||
animated: e.roles.includes('registrant')
|
||||
}))
|
||||
}
|
||||
|
||||
export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
export const domainNSToEdges = (d: Domain): Edge[] => d.nameservers
|
||||
.map(ns => ({
|
||||
id: `ns-${d.ldhName}-${ns.ldhName}`,
|
||||
source: d.ldhName,
|
||||
@@ -34,7 +35,7 @@ export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
label: 'DNS'
|
||||
}))
|
||||
|
||||
export const tldToEdge = (d: Domain) => ({
|
||||
export const tldToEdge = (d: Domain): Edge => ({
|
||||
id: `tld-${d.ldhName}-${d.tld.tld}`,
|
||||
source: d.tld.tld,
|
||||
target: d.ldhName,
|
||||
@@ -42,7 +43,7 @@ export const tldToEdge = (d: Domain) => ({
|
||||
label: t`Registry`
|
||||
})
|
||||
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false): Edge[] {
|
||||
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
|
||||
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
|
||||
const tldEdge = watchlist.domains.map(tldToEdge)
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
|
||||
import React from "react";
|
||||
import {Domain, Nameserver, Tld, Watchlist} from '../../../../utils/api'
|
||||
import React from 'react'
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {entityToName} from "../../../../utils/functions/entityToName";
|
||||
import {entityToName} from '../../../../utils/functions/entityToName'
|
||||
import {Node} from '@xyflow/react'
|
||||
|
||||
export const domainToNode = (d: Domain) => ({
|
||||
export const domainToNode = (d: Domain): Node => ({
|
||||
id: d.ldhName,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: <b>{d.ldhName}</b>},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false): Node[] => {
|
||||
const sponsor = d.entities.find(e => !e.deleted && e.roles.includes('sponsor'))
|
||||
return d.entities
|
||||
.filter(e =>
|
||||
!e.deleted &&
|
||||
(withRegistrar || !e.roles.includes('registrar')) &&
|
||||
(!sponsor || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
((sponsor == null) || !e.roles.includes('registrar') || e.roles.includes('sponsor'))
|
||||
)
|
||||
.map(e => {
|
||||
return {
|
||||
id: e.entity.handle,
|
||||
position: {x: 0, y: 0},
|
||||
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
|
||||
data: {label: entityToName(e)},
|
||||
style: {
|
||||
@@ -32,8 +35,9 @@ export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const tldToNode = (tld: Tld) => ({
|
||||
export const tldToNode = (tld: Tld): Node => ({
|
||||
id: tld.tld,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: t`.${tld.tld} Registry`},
|
||||
type: 'input',
|
||||
style: {
|
||||
@@ -41,8 +45,9 @@ export const tldToNode = (tld: Tld) => ({
|
||||
}
|
||||
})
|
||||
|
||||
export const nsToNode = (ns: Nameserver) => ({
|
||||
export const nsToNode = (ns: Nameserver): Node => ({
|
||||
id: ns.ldhName,
|
||||
position: {x: 0, y: 0},
|
||||
data: {label: ns.ldhName},
|
||||
type: 'output',
|
||||
style: {
|
||||
@@ -50,12 +55,11 @@ export const nsToNode = (ns: Nameserver) => ({
|
||||
}
|
||||
})
|
||||
|
||||
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
|
||||
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false): Node[] {
|
||||
const domains = watchlist.domains.map(domainToNode)
|
||||
const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())]
|
||||
const tlds = [...new Set(watchlist.domains.map(d => d.tld))].filter(t => t.tld !== '.').map(tldToNode)
|
||||
const nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar)
|
||||
|
||||
return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user