From 24e3bc19ff6cb35cca25ae1493d369f9600bdaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Sat, 25 Oct 2025 19:23:15 +0200 Subject: [PATCH] feat: user can enable/disable a watchlist --- .../watchlist/CreateWatchlistButton.tsx | 59 +++++++++++++++++++ .../watchlist/DisableWatchlistButton.tsx | 33 +++++++++++ .../watchlist/UpdateWatchlistButton.tsx | 8 +-- .../tracking/watchlist/WatchlistCard.tsx | 17 ++++-- .../tracking/watchlist/WatchlistsList.tsx | 8 +-- assets/pages/tracking/WatchlistPage.tsx | 44 +++++++------- assets/utils/api/index.ts | 2 + assets/utils/api/watchlist.ts | 12 ++++ migrations/Version20251025160938.php | 31 ++++++++++ src/Entity/Watchlist.php | 23 ++++++++ .../ProcessWatchlistTriggerHandler.php | 2 +- src/Repository/WatchlistRepository.php | 8 +++ tests/State/WatchlistUpdateProcessorTest.php | 2 + 13 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 assets/components/tracking/watchlist/CreateWatchlistButton.tsx create mode 100644 assets/components/tracking/watchlist/DisableWatchlistButton.tsx create mode 100644 migrations/Version20251025160938.php diff --git a/assets/components/tracking/watchlist/CreateWatchlistButton.tsx b/assets/components/tracking/watchlist/CreateWatchlistButton.tsx new file mode 100644 index 0000000..13e6863 --- /dev/null +++ b/assets/components/tracking/watchlist/CreateWatchlistButton.tsx @@ -0,0 +1,59 @@ +import {Button, Drawer, Form} from 'antd' +import {t} from 'ttag' +import {WatchlistForm} from './WatchlistForm' +import React, {useState} from 'react' +import type {Connector} from '../../../utils/api/connectors' +import useBreakpoint from "../../../hooks/useBreakpoint" + +export function CreateWatchlistButton({onUpdateWatchlist, connectors}: { + onUpdateWatchlist: (values: { + domains: string[], + trackedEvents: string[], + trackedEppStatus: string[], + token: string + }) => Promise + connectors: Array +}) { + const [form] = Form.useForm() + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const sm = useBreakpoint('sm') + + const showDrawer = () => setOpen(true) + + const onClose = () => { + setOpen(false) + setLoading(false) + } + + return ( + <> + + {t`Cancel`}} + > + { + setLoading(true) + onUpdateWatchlist(values).then(onClose).catch(() => setLoading(false)) + }} + connectors={connectors} + isCreation + /> + + + ) +} diff --git a/assets/components/tracking/watchlist/DisableWatchlistButton.tsx b/assets/components/tracking/watchlist/DisableWatchlistButton.tsx new file mode 100644 index 0000000..a07ae2f --- /dev/null +++ b/assets/components/tracking/watchlist/DisableWatchlistButton.tsx @@ -0,0 +1,33 @@ +import {Popconfirm, theme, Typography} from 'antd' +import {t} from 'ttag' +import type { Watchlist} from '../../../utils/api' +import {patchWatchlist} from '../../../utils/api' +import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons' +import React from 'react' + +export function DisableWatchlistButton({watchlist, onChange, enabled}: { + watchlist: Watchlist, + onChange: () => void, + enabled: boolean +}) { + const {token} = theme.useToken() + + return ( + enabled ? + await patchWatchlist(watchlist.token, {enabled: !enabled}).then(onChange)} + okText={t`Yes`} + cancelText={t`No`} + okButtonProps={{danger: true}} + > + + + + : + await patchWatchlist(watchlist.token, {enabled: !enabled}).then(onChange)}/> + + ) +} diff --git a/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx b/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx index 215388d..ec090a2 100644 --- a/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx +++ b/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx @@ -5,6 +5,7 @@ import React, {useState} from 'react' import {EditOutlined} from '@ant-design/icons' import type {Connector} from '../../../utils/api/connectors' import type {Watchlist} from '../../../utils/api' +import useBreakpoint from "../../../hooks/useBreakpoint" export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: { watchlist: Watchlist @@ -14,10 +15,9 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors} const [form] = Form.useForm() const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) + const sm = useBreakpoint('sm') - const showDrawer = () => { - setOpen(true) - } + const showDrawer = () => setOpen(true) const onClose = () => { setOpen(false) @@ -44,7 +44,7 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors} Promise connectors: Array - onDelete: () => void + onChange: () => void }) { const rdapEventNameTranslated = rdapEventNameTranslation() const rdapEventDetailTranslated = rdapEventDetailTranslation() @@ -36,7 +37,14 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet return ( <> { (watchlist.connector != null) @@ -52,7 +60,6 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet } size='small' - style={{width: '100%'}} extra={ @@ -65,7 +72,9 @@ export function WatchlistCard({watchlist, onUpdateWatchlist, connectors, onDelet connectors={connectors} /> - + + } > diff --git a/assets/components/tracking/watchlist/WatchlistsList.tsx b/assets/components/tracking/watchlist/WatchlistsList.tsx index 4027936..576e63a 100644 --- a/assets/components/tracking/watchlist/WatchlistsList.tsx +++ b/assets/components/tracking/watchlist/WatchlistsList.tsx @@ -3,21 +3,21 @@ import type {Connector} from '../../../utils/api/connectors' import {WatchlistCard} from './WatchlistCard' import type {Watchlist} from '../../../utils/api' -export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: { +export function WatchlistsList({watchlists, onChange, onUpdateWatchlist, connectors}: { watchlists: Watchlist[] - onDelete: () => void + onChange: () => void onUpdateWatchlist: (values: { domains: string[], trackedEvents: string[], trackedEppStatus: string[], token: string }) => Promise connectors: Array }) { return ( <> - {watchlists.map(watchlist => + {[...watchlists.filter(w => w.enabled), ...watchlists.filter(w => !w.enabled)].map(watchlist => )} diff --git a/assets/pages/tracking/WatchlistPage.tsx b/assets/pages/tracking/WatchlistPage.tsx index 61f678a..1e9a8a4 100644 --- a/assets/pages/tracking/WatchlistPage.tsx +++ b/assets/pages/tracking/WatchlistPage.tsx @@ -1,15 +1,15 @@ import React, {useEffect, useState} from 'react' -import {Card, Divider, Flex, Form, message} from 'antd' +import {Divider, Flex, Form, message} from 'antd' import type {Watchlist} from '../../utils/api' import {getWatchlists, postWatchlist, putWatchlist} from '../../utils/api' import type {AxiosError} from 'axios' import {t} from 'ttag' -import {WatchlistForm} from '../../components/tracking/watchlist/WatchlistForm' import {WatchlistsList} from '../../components/tracking/watchlist/WatchlistsList' import type {Connector} from '../../utils/api/connectors' import { getConnectors} from '../../utils/api/connectors' import {showErrorAPI} from '../../utils/functions/showErrorAPI' +import {CreateWatchlistButton} from "../../components/tracking/watchlist/CreateWatchlistButton" interface FormValuesType { name?: string @@ -20,17 +20,15 @@ interface FormValuesType { dsn?: string[] } -const getRequestDataFromFormCreation = (values: FormValuesType) => { - const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase()) - return { - name: values.name, - domains: domainsURI, +const getRequestDataFromFormCreation = (values: FormValuesType) => + ({ name: values.name, + domains: values.domains.map(d => '/api/domains/' + d.toLowerCase()), trackedEvents: values.trackedEvents, trackedEppStatus: values.trackedEppStatus, connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined, - dsn: values.dsn - } -} + dsn: values.dsn, + enabled: true + }) export default function WatchlistPage() { const [form] = Form.useForm() @@ -38,15 +36,13 @@ export default function WatchlistPage() { const [watchlists, setWatchlists] = useState() const [connectors, setConnectors] = useState>() - const onCreateWatchlist = (values: FormValuesType) => { - postWatchlist(getRequestDataFromFormCreation(values)).then(() => { + const onCreateWatchlist = async (values: FormValuesType) => await postWatchlist(getRequestDataFromFormCreation(values)).then(() => { form.resetFields() refreshWatchlists() messageApi.success(t`Watchlist created !`) }).catch((e: AxiosError) => { showErrorAPI(e, messageApi) }) - } const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => await putWatchlist({ token: values.token, @@ -78,18 +74,18 @@ export default function WatchlistPage() { return ( {contextHolder} - - {(connectors != null) && - } - - + {(connectors != null) && (watchlists != null) && watchlists.length > 0 && - } + <> + + + + } ) } diff --git a/assets/utils/api/index.ts b/assets/utils/api/index.ts index 7bd2b0c..4b09dd5 100644 --- a/assets/utils/api/index.ts +++ b/assets/utils/api/index.ts @@ -84,6 +84,7 @@ export interface WatchlistRequest { trackedEppStatus?: string[] connector?: string dsn?: string[] + enabled?: boolean } export interface Watchlist { @@ -100,6 +101,7 @@ export interface Watchlist { createdAt: string } createdAt: string + enabled: boolean } export interface InstanceConfig { diff --git a/assets/utils/api/watchlist.ts b/assets/utils/api/watchlist.ts index dcdcad4..2dbf8f9 100644 --- a/assets/utils/api/watchlist.ts +++ b/assets/utils/api/watchlist.ts @@ -32,6 +32,18 @@ export async function postWatchlist(watchlist: WatchlistRequest) { return response.data } +export async function patchWatchlist(token: string, watchlist: Partial) { + const response = await request<{ token: string }>({ + method: 'PATCH', + url: 'watchlists/' + token, + data: watchlist, + headers: { + 'Content-Type': 'application/merge-patch+json' + } + }) + return response.data +} + export async function deleteWatchlist(token: string): Promise { await request({ method: 'DELETE', diff --git a/migrations/Version20251025160938.php b/migrations/Version20251025160938.php new file mode 100644 index 0000000..c157bb6 --- /dev/null +++ b/migrations/Version20251025160938.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE watchlist ADD enabled BOOLEAN NOT NULL DEFAULT true'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE watchlist DROP enabled'); + } +} diff --git a/src/Entity/Watchlist.php b/src/Entity/Watchlist.php index ed243c4..2875eda 100644 --- a/src/Entity/Watchlist.php +++ b/src/Entity/Watchlist.php @@ -6,6 +6,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use App\Repository\WatchlistRepository; @@ -95,6 +96,12 @@ use Symfony\Component\Validator\Constraints as Assert; security: 'object.getUser() == user', processor: WatchlistUpdateProcessor::class, ), + new Patch( + normalizationContext: ['groups' => 'watchlist:list'], + denormalizationContext: ['groups' => ['watchlist:update']], + security: 'object.getUser() == user', + processor: WatchlistUpdateProcessor::class, + ), new Delete( security: 'object.getUser() == user' ), @@ -208,6 +215,10 @@ class Watchlist ])] private array $trackedEppStatus = []; + #[ORM\Column(type: Types::BOOLEAN)] + #[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])] + private ?bool $enabled = null; + public function __construct() { $this->token = Uuid::v4(); @@ -332,4 +343,16 @@ class Watchlist return $this; } + + public function isEnabled(): ?bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): static + { + $this->enabled = $enabled; + + return $this; + } } diff --git a/src/MessageHandler/ProcessWatchlistTriggerHandler.php b/src/MessageHandler/ProcessWatchlistTriggerHandler.php index 1cc18eb..52010ad 100644 --- a/src/MessageHandler/ProcessWatchlistTriggerHandler.php +++ b/src/MessageHandler/ProcessWatchlistTriggerHandler.php @@ -31,7 +31,7 @@ final readonly class ProcessWatchlistTriggerHandler */ $randomizer = new Randomizer(); - $watchlists = $randomizer->shuffleArray($this->watchlistRepository->findAll()); + $watchlists = $randomizer->shuffleArray($this->watchlistRepository->getEnabledWatchlist()); /** @var Watchlist $watchlist */ foreach ($watchlists as $watchlist) { diff --git a/src/Repository/WatchlistRepository.php b/src/Repository/WatchlistRepository.php index caaab59..fb6bfdd 100644 --- a/src/Repository/WatchlistRepository.php +++ b/src/Repository/WatchlistRepository.php @@ -25,6 +25,14 @@ class WatchlistRepository extends ServiceEntityRepository ->getQuery()->getSingleScalarResult(); } + public function getEnabledWatchlist() + { + return $this->createQueryBuilder('w') + ->select() + ->where('w.isEnabled = true') + ->getQuery()->execute(); + } + // /** // * @return Watchlist[] Returns an array of Watchlist objects // */ diff --git a/tests/State/WatchlistUpdateProcessorTest.php b/tests/State/WatchlistUpdateProcessorTest.php index 7d4c1a2..997a6bf 100644 --- a/tests/State/WatchlistUpdateProcessorTest.php +++ b/tests/State/WatchlistUpdateProcessorTest.php @@ -46,6 +46,7 @@ final class WatchlistUpdateProcessorTest extends ApiTestCase 'name' => 'My modified Watchlist', 'trackedEvents' => ['last changed'], 'trackedEppStatus' => [], + 'enabled' => true ]]); $this->assertResponseIsSuccessful(); $this->assertMatchesResourceItemJsonSchema(Watchlist::class); @@ -64,6 +65,7 @@ final class WatchlistUpdateProcessorTest extends ApiTestCase 'trackedEvents' => ['last changed', 'transfer', 'expiration', 'deletion'], 'trackedEppStatus' => ['redemption period', 'pending delete', 'client hold', 'server hold'], 'connector' => $connectorId, + 'enabled' => true, ]]); return $client;