mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
Merged master
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
import {Card, Divider, Popconfirm, Space, Table, Tag, theme, Typography} from "antd";
|
||||
import {t} from "ttag";
|
||||
import {deleteWatchlist} from "../../utils/api";
|
||||
import {CalendarFilled, DeleteFilled, DisconnectOutlined, LinkOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import useBreakpoint from "../../hooks/useBreakpoint";
|
||||
import {actionToColor, domainEvent} from "../search/EventTimeline";
|
||||
import {Watchlist} from "../../pages/tracking/WatchlistPage";
|
||||
import punycode from "punycode/punycode";
|
||||
|
||||
const {useToken} = theme;
|
||||
|
||||
export function WatchlistsList({watchlists, onDelete}: { watchlists: Watchlist[], onDelete: () => void }) {
|
||||
const {token} = useToken()
|
||||
const sm = useBreakpoint('sm')
|
||||
|
||||
const domainEventTranslated = domainEvent()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t`Domain names`,
|
||||
dataIndex: 'domains'
|
||||
},
|
||||
{
|
||||
title: t`Tracked events`,
|
||||
dataIndex: 'events'
|
||||
}
|
||||
]
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<>
|
||||
<Card
|
||||
hoverable
|
||||
title={<>
|
||||
{
|
||||
watchlist.connector ?
|
||||
<Tag icon={<LinkOutlined/>} color="lime-inverse" title={watchlist.connector.id}/> :
|
||||
<Tag icon={<DisconnectOutlined/>} color="default"
|
||||
title={t`This Watchlist is not linked to a Connector.`}/>
|
||||
}
|
||||
<Typography.Text title={new Date(watchlist.createdAt).toLocaleString()}>
|
||||
{t`Watchlist` + (watchlist.name ? ` (${watchlist.name})` : '')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
size='small'
|
||||
style={{width: '100%'}}
|
||||
extra={<Space size='middle'>
|
||||
<Typography.Link href={`/api/watchlists/${watchlist.token}/calendar`}>
|
||||
<CalendarFilled title={t`Export events to iCalendar format`}/>
|
||||
</Typography.Link>
|
||||
<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}}>
|
||||
<DeleteFilled style={{color: token.colorError}} title={t`Delete the Watchlist`}/>
|
||||
</Popconfirm>
|
||||
</Space>}
|
||||
>
|
||||
<Card.Meta description={watchlist.token} style={{marginBottom: '1em'}}/>
|
||||
<Table
|
||||
size='small'
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
style={{width: '100%'}}
|
||||
dataSource={[{
|
||||
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
|
||||
events: watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tag color={actionToColor(t.event)}>
|
||||
{domainEventTranslated[t.event as keyof typeof domainEventTranslated]}
|
||||
</Tag>
|
||||
)
|
||||
}]}
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Button, Checkbox, Form, FormInstance, Input, Popconfirm, Select, Space, Typography} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {Connector, ConnectorProvider} from "../../utils/api/connectors";
|
||||
import {Connector, ConnectorProvider} from "../../../utils/api/connectors";
|
||||
import {t} from "ttag";
|
||||
import {BankOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
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},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate: (values: Connector) => void }) {
|
||||
const [provider, setProvider] = useState<string>()
|
||||
@@ -186,7 +186,7 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item style={{marginTop: 10}}>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Card, Divider, Popconfirm, theme, Typography} from "antd";
|
||||
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";
|
||||
import {Connector, deleteConnector} from "../../../utils/api/connectors";
|
||||
|
||||
const {useToken} = theme;
|
||||
|
||||
@@ -11,17 +11,23 @@ export type ConnectorElement = Connector & { id: string, createdAt: string }
|
||||
|
||||
export function ConnectorsList({connectors, onDelete}: { connectors: ConnectorElement[], onDelete: () => void }) {
|
||||
const {token} = useToken()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const onConnectorDelete = (connector: ConnectorElement) => 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={() => deleteConnector(connector.id).then(onDelete)}
|
||||
onConfirm={() => onConnectorDelete(connector)}
|
||||
okText={t`Yes`}
|
||||
cancelText={t`No`}
|
||||
><DeleteFilled style={{color: token.colorError}}/></Popconfirm>}>
|
||||
@@ -0,0 +1,22 @@
|
||||
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";
|
||||
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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";
|
||||
|
||||
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>
|
||||
}
|
||||
79
assets/components/tracking/watchlist/TrackedDomainTable.tsx
Normal file
79
assets/components/tracking/watchlist/TrackedDomainTable.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Domain, getTrackedDomainList} from "../../../utils/api";
|
||||
import {Table, Tag, Tooltip} from "antd";
|
||||
import {t} from "ttag";
|
||||
import useBreakpoint from "../../../hooks/useBreakpoint";
|
||||
import {ColumnType} from "antd/es/table";
|
||||
import {rdapStatusCodeDetailTranslation} from "../../../utils/functions/rdapTranslation";
|
||||
import {eppStatusCodeToColor} from "../../../utils/functions/eppStatusCodeToColor";
|
||||
|
||||
export function TrackedDomainTable() {
|
||||
const sm = useBreakpoint('sm')
|
||||
const [dataTable, setDataTable] = useState<Domain[]>([])
|
||||
const [total, setTotal] = useState()
|
||||
|
||||
|
||||
const rdapStatusCodeDetailTranslated = rdapStatusCodeDetailTranslation()
|
||||
|
||||
const fetchData = (params: { page: number, itemsPerPage: number }) => {
|
||||
getTrackedDomainList(params).then(data => {
|
||||
setTotal(data['hydra:totalItems'])
|
||||
setDataTable(data['hydra:member'].map((d: Domain) => {
|
||||
const expirationDate = d.events.find(e => e.action === 'expiration' && !e.deleted)?.date
|
||||
|
||||
return {
|
||||
key: d.ldhName,
|
||||
ldhName: d.ldhName,
|
||||
expirationDate: expirationDate ? new Date(expirationDate).toLocaleString() : '-',
|
||||
status: d.status.map(s => <Tooltip
|
||||
placement='bottomLeft'
|
||||
title={s in rdapStatusCodeDetailTranslated ? rdapStatusCodeDetailTranslated[s as keyof typeof rdapStatusCodeDetailTranslated] : undefined}>
|
||||
<Tag color={eppStatusCodeToColor(s)}>{s}</Tag>
|
||||
</Tooltip>
|
||||
),
|
||||
updatedAt: new Date(d.updatedAt).toLocaleString()
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData({page: 1, itemsPerPage: 30})
|
||||
}, [])
|
||||
|
||||
const columns: ColumnType<any>[] = [
|
||||
{
|
||||
title: t`Domain`,
|
||||
dataIndex: "ldhName"
|
||||
},
|
||||
{
|
||||
title: t`Expiration date`,
|
||||
dataIndex: 'expirationDate'
|
||||
},
|
||||
{
|
||||
title: t`Status`,
|
||||
dataIndex: 'status'
|
||||
},
|
||||
{
|
||||
title: t`Updated at`,
|
||||
dataIndex: 'updatedAt'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
return <Table
|
||||
loading={total === undefined}
|
||||
columns={columns}
|
||||
dataSource={dataTable}
|
||||
pagination={{
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
defaultPageSize: 30,
|
||||
onChange: (page, itemsPerPage) => {
|
||||
fetchData({page, itemsPerPage})
|
||||
}
|
||||
}}
|
||||
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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";
|
||||
|
||||
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[]
|
||||
}) {
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
|
||||
const showDrawer = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
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))
|
||||
}}
|
||||
connectors={connectors}
|
||||
isCreation={false}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
}
|
||||
96
assets/components/tracking/watchlist/WatchlistCard.tsx
Normal file
96
assets/components/tracking/watchlist/WatchlistCard.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {Card, Divider, Space, Table, 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 punycode from "punycode/punycode";
|
||||
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 {actionToColor} from "../../../utils/functions/actionToColor";
|
||||
|
||||
export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelete}: {
|
||||
watchlist: Watchlist,
|
||||
onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
|
||||
connectors: (Connector & { id: string })[],
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const sm = useBreakpoint('sm')
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t`Domain names`,
|
||||
dataIndex: 'domains'
|
||||
},
|
||||
{
|
||||
title: t`Tracked events`,
|
||||
dataIndex: 'events'
|
||||
}
|
||||
]
|
||||
|
||||
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'}}/>
|
||||
<Table
|
||||
size='small'
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
style={{width: '100%'}}
|
||||
dataSource={[{
|
||||
domains: watchlist.domains.map(d => <Tag>{punycode.toUnicode(d.ldhName)}</Tag>),
|
||||
events: watchlist.triggers?.filter(t => t.action === 'email')
|
||||
.map(t => <Tooltip
|
||||
title={t.event in rdapEventDetailTranslated ? rdapEventDetailTranslated[t.event as keyof typeof rdapEventDetailTranslated] : undefined}>
|
||||
<Tag color={actionToColor(t.event)}>
|
||||
{rdapEventNameTranslated[t.event as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}]}
|
||||
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
|
||||
/>
|
||||
</Card>
|
||||
<Divider/>
|
||||
</>
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag} from "antd";
|
||||
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 {actionToColor, domainEvent} from "../search/EventTimeline";
|
||||
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";
|
||||
|
||||
type TagRender = SelectProps['tagRender'];
|
||||
|
||||
@@ -25,38 +27,48 @@ const formItemLayoutWithOutLabel = {
|
||||
},
|
||||
};
|
||||
|
||||
export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
|
||||
form: FormInstance,
|
||||
connectors: (Connector & { id: string })[]
|
||||
onCreateWatchlist: (values: { domains: string[], emailTriggers: string[] }) => void
|
||||
onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
|
||||
isCreation: boolean
|
||||
}) {
|
||||
const domainEventTranslated = domainEvent()
|
||||
const rdapEventNameTranslated = rdapEventNameTranslation()
|
||||
const rdapEventDetailTranslated = rdapEventDetailTranslation()
|
||||
|
||||
const triggerTagRenderer: TagRender = (props) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
return (
|
||||
<Tag
|
||||
color={actionToColor(value)}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginInlineEnd: 4}}
|
||||
>
|
||||
{domainEventTranslated[value as keyof typeof domainEventTranslated]}
|
||||
</Tag>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return (<Tooltip
|
||||
title={value in rdapEventDetailTranslated ? rdapEventDetailTranslated[value as keyof typeof rdapEventDetailTranslated] : undefined}>
|
||||
<Tag
|
||||
icon={actionToIcon(value)}
|
||||
color={actionToColor(value)}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginInlineEnd: 4}}
|
||||
>
|
||||
{rdapEventNameTranslated[value as keyof typeof rdapEventNameTranslated]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return <Form
|
||||
{...formItemLayoutWithOutLabel}
|
||||
form={form}
|
||||
onFinish={onCreateWatchlist}
|
||||
initialValues={{emailTriggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
onFinish={onFinish}
|
||||
initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
|
||||
>
|
||||
|
||||
<Form.Item name='token' hidden>
|
||||
<Input hidden/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t`Name`}
|
||||
name='name'
|
||||
labelCol={{
|
||||
@@ -134,7 +146,7 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item label={t`Tracked events`}
|
||||
name='emailTriggers'
|
||||
name='triggers'
|
||||
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
|
||||
labelCol={{
|
||||
xs: {span: 24},
|
||||
@@ -150,9 +162,10 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
mode="multiple"
|
||||
tagRender={triggerTagRenderer}
|
||||
style={{width: '100%'}}
|
||||
options={Object.keys(domainEventTranslated).map(e => ({
|
||||
options={Object.keys(rdapEventNameTranslated).map(e => ({
|
||||
value: e,
|
||||
label: domainEventTranslated[e as keyof typeof domainEventTranslated]
|
||||
title: e in rdapEventDetailTranslated ? rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] : undefined,
|
||||
label: rdapEventNameTranslated[e as keyof typeof rdapEventNameTranslated]
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -180,10 +193,63 @@ export function WatchlistForm({form, connectors, onCreateWatchlist}: {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<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
|
||||
{...field}
|
||||
validateTrigger={['onChange', 'onBlur']}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: t`Required`
|
||||
}, {
|
||||
pattern: /:\/\//,
|
||||
message: t`This DSN does not appear to be valid`
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={'slack://TOKEN@default?channel=CHANNEL'} style={{width: '60%'}}
|
||||
autoComplete='off'/>
|
||||
</Form.Item>
|
||||
{fields.length > 0 ? (
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item help={
|
||||
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'
|
||||
target='_blank'>
|
||||
{t`Check out this link to the Symfony documentation to help you build the DSN`}
|
||||
</Typography.Link>}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
style={{width: '60%'}}
|
||||
icon={<PlusOutlined/>}
|
||||
>
|
||||
{t`Add a Webhook`}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item style={{marginTop: '5vh'}}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t`Create`}
|
||||
{isCreation ? t`Create` : t`Update`}
|
||||
</Button>
|
||||
<Button type="default" htmlType="reset">
|
||||
{t`Reset`}
|
||||
23
assets/components/tracking/watchlist/WatchlistsList.tsx
Normal file
23
assets/components/tracking/watchlist/WatchlistsList.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import {Watchlist} from "../../../pages/tracking/WatchlistPage";
|
||||
import {Connector} from "../../../utils/api/connectors";
|
||||
import {WatchlistCard} from "./WatchlistCard";
|
||||
|
||||
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 })[]
|
||||
}) {
|
||||
|
||||
|
||||
return <>
|
||||
{watchlists.map(watchlist =>
|
||||
<WatchlistCard watchlist={watchlist}
|
||||
onUpdateWatchlist={onUpdateWatchlist}
|
||||
connectors={connectors}
|
||||
onDelete={onDelete}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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";
|
||||
|
||||
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([])
|
||||
|
||||
useEffect(() => {
|
||||
setEdges([])
|
||||
setNodes([])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setLoading(true)
|
||||
getWatchlist(token).then(w => {
|
||||
const e = getLayoutedElements(watchlistToNodes(w, true), watchlistToEdges(w, true))
|
||||
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>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import dagre from "dagre"
|
||||
|
||||
export const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const nodeWidth = 172
|
||||
const nodeHeight = 200
|
||||
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({rankdir: direction});
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
dagreGraph.setNode(node.id, {width: nodeWidth, height: nodeHeight});
|
||||
});
|
||||
|
||||
edges.forEach((edge: any) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
const newNodes = nodes.map((node: any) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? 'left' : 'top',
|
||||
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {nodes: newNodes, edges};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {Domain, Watchlist} from "../../../../utils/api";
|
||||
import {rdapRoleTranslation} from "../../../../utils/functions/rdapTranslation";
|
||||
import {t} from "ttag";
|
||||
|
||||
import {rolesToColor} from "../../../../utils/functions/rolesToColor";
|
||||
|
||||
export function domainEntitiesToEdges(d: Domain, withRegistrar = false) {
|
||||
const rdapRoleTranslated = rdapRoleTranslation()
|
||||
return d.entities
|
||||
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
|
||||
.map(e => ({
|
||||
id: `e-${d.ldhName}-${e.entity.handle}`,
|
||||
source: e.roles.includes('registrant') || e.roles.includes('registrar') ? e.entity.handle : d.ldhName,
|
||||
target: e.roles.includes('registrant') || e.roles.includes('registrar') ? d.ldhName : e.entity.handle,
|
||||
style: {stroke: rolesToColor(e.roles), strokeWidth: 3},
|
||||
label: e.roles
|
||||
.map(r => r in rdapRoleTranslated ? rdapRoleTranslated[r as keyof typeof rdapRoleTranslated] : r)
|
||||
.join(', '),
|
||||
animated: e.roles.includes('registrant'),
|
||||
}))
|
||||
}
|
||||
|
||||
export const domainNSToEdges = (d: Domain) => d.nameservers
|
||||
.map(ns => ({
|
||||
id: `ns-${d.ldhName}-${ns.ldhName}`,
|
||||
source: d.ldhName,
|
||||
target: ns.ldhName,
|
||||
style: {stroke: 'grey', strokeWidth: 3},
|
||||
label: 'DNS'
|
||||
}))
|
||||
|
||||
export const tldToEdge = (d: Domain) => ({
|
||||
id: `tld-${d.ldhName}-${d.tld.tld}`,
|
||||
source: d.tld.tld,
|
||||
target: d.ldhName,
|
||||
style: {stroke: 'yellow', strokeWidth: 3},
|
||||
label: t`Registry`
|
||||
})
|
||||
|
||||
export function watchlistToEdges(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
const entitiesEdges = watchlist.domains.map(d => domainEntitiesToEdges(d, withRegistrar)).flat()
|
||||
const nameserversEdges = watchlist.domains.map(domainNSToEdges).flat()
|
||||
const tldEdge = watchlist.domains.map(tldToEdge)
|
||||
|
||||
return [...entitiesEdges, ...nameserversEdges, ...(withTld ? tldEdge : [])]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {Domain, Nameserver, Tld, Watchlist} from "../../../../utils/api";
|
||||
import React from "react";
|
||||
import {t} from 'ttag'
|
||||
|
||||
import {entityToName} from "../../../../utils/functions/entityToName";
|
||||
|
||||
export const domainToNode = (d: Domain) => ({
|
||||
id: d.ldhName,
|
||||
data: {label: <b>{d.ldhName}</b>},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const domainEntitiesToNode = (d: Domain, withRegistrar = false) => d.entities
|
||||
.filter(e => !e.deleted && (!withRegistrar ? !e.roles.includes('registrar') : true))
|
||||
.map(e => {
|
||||
return {
|
||||
id: e.entity.handle,
|
||||
type: e.roles.includes('registrant') || e.roles.includes('registrar') ? 'input' : 'output',
|
||||
data: {label: entityToName(e)},
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const tldToNode = (tld: Tld) => ({
|
||||
id: tld.tld,
|
||||
data: {label: t`.${tld.tld} Registry`},
|
||||
type: 'input',
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export const nsToNode = (ns: Nameserver) => ({
|
||||
id: ns.ldhName,
|
||||
data: {label: ns.ldhName},
|
||||
type: 'output',
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
})
|
||||
|
||||
export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, withTld = false) {
|
||||
|
||||
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))].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