feat: udpate watchlist entity diagram

This commit is contained in:
Maël Gangloff
2024-08-16 13:56:52 +02:00
parent e21df5a137
commit d82aac451c
14 changed files with 223 additions and 203 deletions

View File

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

View File

@@ -0,0 +1,67 @@
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[], emailTriggers: 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: 'emailTriggers', value: watchlist.triggers?.map(t => t.event)},
])
}}/>
</Typography.Link>
<Drawer
title={t`Update a Watchlist`}
width='80%'
onClose={onClose}
open={open}
loading={loading}
styles={{
body: {
paddingBottom: 80,
}
}}
extra={<Button onClick={onClose}>{t`Cancel`}</Button>}
>
<WatchlistForm
form={form}
onFinish={values => {
setLoading(true)
onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false))
}}
connectors={connectors}
isCreation={false}
/>
</Drawer>
</>
}

View File

@@ -0,0 +1,200 @@
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag} 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";
type TagRender = SelectProps['tagRender'];
const formItemLayout = {
labelCol: {
xs: {span: 24},
sm: {span: 4},
},
wrapperCol: {
xs: {span: 24},
sm: {span: 20},
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {span: 24, offset: 0},
sm: {span: 20, offset: 4},
},
};
export function WatchlistForm({form, connectors, onFinish, isCreation}: {
form: FormInstance,
connectors: (Connector & { id: string })[]
onFinish: (values: { domains: string[], emailTriggers: string[], token: string }) => void
isCreation: boolean
}) {
const domainEventTranslated = domainEvent()
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>
)
}
return <Form
{...formItemLayoutWithOutLabel}
form={form}
onFinish={onFinish}
initialValues={{emailTriggers: ['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},
}}
>
<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`));
}
},
},
]}
>
{(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}
>
<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>
))}
<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='emailTriggers'
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(domainEventTranslated).map(e => ({
value: e,
label: domainEventTranslated[e as keyof typeof domainEventTranslated]
}))}
/>
</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
allowClear
placeholder={t`Connector`}
suffixIcon={<ApiOutlined/>}
optionFilterProp="label"
options={connectors.map(c => ({
label: `${c.provider} (${c.id})`,
value: c.id
}))}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{isCreation ? t`Create` : t`Update`}
</Button>
<Button type="default" htmlType="reset">
{t`Reset`}
</Button>
</Space>
</Form.Item>
</Form>
}

View File

@@ -0,0 +1,95 @@
import {Card, Divider, Space, Table, Tag, Typography} from "antd";
import {t} from "ttag";
import {CalendarFilled, 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";
import {Connector} from "../../../utils/api/connectors";
import {UpdateWatchlistButton} from "./UpdateWatchlistButton";
import {DeleteWatchlistButton} from "./DeleteWatchlistButton";
import {ViewDiagramWatchlistButton} from "../diagram/ViewDiagramWatchlistButton";
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
watchlists: Watchlist[],
onDelete: () => void,
onUpdateWatchlist: (values: { domains: string[], emailTriggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[]
}) {
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'>
<ViewDiagramWatchlistButton token={watchlist.token}/>
<Typography.Link href={`/api/watchlists/${watchlist.token}/calendar`}>
<CalendarFilled title={t`Export events to iCalendar format`}
style={{color: 'limegreen'}}/>
</Typography.Link>
<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 => <Tag color={actionToColor(t.event)}>
{domainEventTranslated[t.event as keyof typeof domainEventTranslated]}
</Tag>
)
}]}
{...(sm ? {scroll: {y: 'max-content'}} : {scroll: {y: 240}})}
/>
</Card>
<Divider/>
</>
)}
</>
}