feat: add watchlist entity diagram

This commit is contained in:
Maël Gangloff 2024-08-16 03:54:48 +02:00
parent d0d13a07a9
commit 50b76c8438
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
7 changed files with 345 additions and 569 deletions

View File

@ -1,12 +1,119 @@
import {Button, Modal, Space, Typography} from "antd"
import {Button, Flex, Modal, Space, Typography} from "antd"
import {t} from "ttag"
import React, {useState} from "react"
import {Watchlist} from "../../pages/tracking/WatchlistPage"
import React, {useEffect, useState} from "react"
import {ApartmentOutlined} from "@ant-design/icons"
export function ViewDiagramWatchlistButton({watchlist}: { watchlist: Watchlist }) {
import '@xyflow/react/dist/style.css'
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState} from "@xyflow/react";
import {getWatchlist, Watchlist} from "../../utils/api";
import dagre from 'dagre'
import vCard from "vcf";
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 200;
const getLayoutedElements = (nodes: any, edges: any, direction = 'TB') => {
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};
}
function watchlistToNodes(watchlist: Watchlist) {
const domains = watchlist.domains.map(d => ({
id: d.ldhName,
data: {label: <b>{d.ldhName}</b>},
style: {
width: 200
}
}))
const entities = [...new Set(watchlist.domains
.map(d => d.entities
.filter(e => !e.roles.includes('registrar'))
.map(e => e.entity
)
).flat())].map(e => {
const jCard = vCard.fromJSON(e.jCard)
let label = e.handle
if (jCard.data.fn !== undefined && !Array.isArray(jCard.data.fn)) label = jCard.data.fn.valueOf()
return {
id: e.handle,
data: {label},
style: {
width: 200
}
}
})
return [...domains, ...entities]
}
const rolesToColor = (roles: string[]) => roles.includes('registrant') ? 'green' :
roles.includes('technical') ? 'orange' : 'black'
function watchlistToEdges(watchlist: Watchlist) {
return watchlist.domains
.map(d => d.entities
.filter(e => !e.roles.includes('registrar'))
.map(e => ({
id: `${d.ldhName}-${e.entity.handle}`,
source: e.roles.includes('technical') ? d.ldhName : e.entity.handle,
target: e.roles.includes('technical') ? e.entity.handle : d.ldhName,
style: {stroke: rolesToColor(e.roles), strokeWidth: 3},
animated: e.roles.includes('registrant'),
}))
).flat(2)
}
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(() => {
if (!open) return
setLoading(true)
getWatchlist(token).then(w => {
const e = getLayoutedElements(watchlistToNodes(w), watchlistToEdges(w))
setNodes(e.nodes)
setEdges(e.edges)
}).catch(() => setOpen(false)).finally(() => setLoading(false))
}, [open])
return <>
<Typography.Link>
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
@ -17,13 +124,9 @@ export function ViewDiagramWatchlistButton({watchlist}: { watchlist: Watchlist }
title={t`Watchlist Entity Diagram`}
centered
open={open}
loading={loading}
footer={
<Space>
<Button type="primary" color='violet' onClick={() => {
}}>
Download
</Button>
<Button type="default" onClick={() => setOpen(false)}>
Close
</Button>
@ -31,10 +134,21 @@ export function ViewDiagramWatchlistButton({watchlist}: { watchlist: Watchlist }
}
onOk={() => setOpen(false)}
onCancel={() => setOpen(false)}
width='80%'
width='80vw'
>
{nodes && edges && <Flex style={{width: '75vw', height: '80vh'}}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
style={{width: '100%', height: '100vh'}}
>
<MiniMap/>
<Controls/>
<Background/>
</ReactFlow>
</Flex>}
</Modal>
</>
}

View File

@ -54,7 +54,7 @@ export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connect
style={{width: '100%'}}
extra={<Space size='middle'>
<ViewDiagramWatchlistButton watchlist={watchlist}/>
<ViewDiagramWatchlistButton token={watchlist.token}/>
<Typography.Link href={`/api/watchlists/${watchlist.token}/calendar`}>
<CalendarFilled title={t`Export events to iCalendar format`}

View File

@ -64,13 +64,22 @@ export interface User {
roles: string[]
}
export interface Watchlist {
export interface WatchlistRequest {
name?: string
domains: string[],
triggers: { event: EventAction, action: TriggerAction }[],
connector?: string
}
export interface Watchlist {
token: string
name?: string
domains: Domain[],
triggers: { event: EventAction, action: TriggerAction }[],
connector?: string
createdAt: string
}
export interface InstanceConfig {
ssoLogin: boolean
limtedFeatures: boolean

View File

@ -1,4 +1,4 @@
import {request, Watchlist} from "./index";
import {request, Watchlist, WatchlistRequest} from "./index";
export async function getWatchlists() {
const response = await request({
@ -8,13 +8,13 @@ export async function getWatchlists() {
}
export async function getWatchlist(token: string) {
const response = await request<Watchlist & { token: string }>({
const response = await request<Watchlist>({
url: 'watchlists/' + token
})
return response.data
}
export async function postWatchlist(watchlist: Watchlist) {
export async function postWatchlist(watchlist: WatchlistRequest) {
const response = await request<{ token: string }>({
method: 'POST',
url: 'watchlists',
@ -33,8 +33,8 @@ export async function deleteWatchlist(token: string): Promise<void> {
})
}
export async function putWatchlist(watchlist: Partial<Watchlist> & { token: string }) {
const response = await request<Watchlist>({
export async function putWatchlist(watchlist: Partial<WatchlistRequest> & { token: string }) {
const response = await request<WatchlistRequest>({
method: 'PUT',
url: 'watchlists/' + watchlist.token,
data: watchlist,

View File

@ -21,15 +21,18 @@
"@fontsource/noto-color-emoji": "^5.0.27",
"@symfony/webpack-encore": "^4.0.0",
"@types/axios": "^0.14.0",
"@types/dagre": "^0.7.52",
"@types/jsonld": "^1.5.15",
"@types/punycode": "^2.1.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-responsive": "^8.0.8",
"@types/vcf": "^2.0.7",
"@xyflow/react": "^12.1.0",
"antd": "^5.19.3",
"axios": "^1.7.2",
"core-js": "^3.23.0",
"dagre": "^0.8.5",
"html-loader": "^5.1.0",
"jsonld": "^8.3.2",
"punycode": "^2.3.1",
@ -57,8 +60,5 @@
"build": "encore production --progress",
"ttag:po2json": "cd translations; for i in $(find . -name \"*.po\"); do ttag po2json $i > ../public/locales/$i.json; done; cd ..",
"ttag:extract": "ttag extract $(find assets -name '*.ts' -or -name '*.tsx') -o translations/translations.pot"
},
"dependencies": {
"remove": "^0.1.5"
}
}

View File

@ -149,7 +149,7 @@ msgid "At least one domain name"
msgstr ""
#: assets/components/tracking/WatchlistForm.tsx:100
#: assets/components/tracking/WatchlistsList.tsx:28
#: assets/components/tracking/WatchlistsList.tsx:26
msgid "Domain names"
msgstr ""
@ -162,7 +162,7 @@ msgid "Add a Domain name"
msgstr ""
#: assets/components/tracking/WatchlistForm.tsx:142
#: assets/components/tracking/WatchlistsList.tsx:32
#: assets/components/tracking/WatchlistsList.tsx:30
msgid "Tracked events"
msgstr ""
@ -195,6 +195,14 @@ msgstr ""
msgid "Reset"
msgstr ""
#: assets/components/tracking/ViewDiagramWatchlistButton.tsx:119
msgid "View the Watchlist Entity Diagram"
msgstr ""
#: assets/components/tracking/ViewDiagramWatchlistButton.tsx:124
msgid "Watchlist Entity Diagram"
msgstr ""
#: assets/components/tracking/ConnectorForm.tsx:40
msgid "Provider"
msgstr ""
@ -265,49 +273,49 @@ msgid ""
"the Provider's API"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:59
msgid "This Watchlist is not linked to a Connector."
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:62
msgid "Watchlist"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:70
msgid "Export events to iCalendar format"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:73
#: assets/components/tracking/UpdateWatchlistButton.tsx:31
msgid "Edit the Watchlist"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:86
#: assets/components/tracking/UpdateWatchlistButton.tsx:43
msgid "Update a Watchlist"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:96
#: assets/components/tracking/UpdateWatchlistButton.tsx:53
msgid "Cancel"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:110
#: assets/components/tracking/WatchlistsList.tsx:117
#: assets/components/tracking/DeleteWatchlistButton.tsx:12
#: assets/components/tracking/DeleteWatchlistButton.tsx:19
msgid "Delete the Watchlist"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:111
#: assets/components/tracking/DeleteWatchlistButton.tsx:13
msgid "Are you sure to delete this Watchlist?"
msgstr ""
#: assets/components/tracking/ConnectorsList.tsx:25
#: assets/components/tracking/WatchlistsList.tsx:113
#: assets/components/tracking/DeleteWatchlistButton.tsx:15
msgid "Yes"
msgstr ""
#: assets/components/tracking/ConnectorsList.tsx:26
#: assets/components/tracking/WatchlistsList.tsx:114
#: assets/components/tracking/DeleteWatchlistButton.tsx:16
msgid "No"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:46
msgid "This Watchlist is not linked to a Connector."
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:49
msgid "Watchlist"
msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:60
msgid "Export events to iCalendar format"
msgstr ""
#: assets/components/tracking/ConnectorsList.tsx:19
#, javascript-format
msgid "Connector ${ connector.provider }"

695
yarn.lock

File diff suppressed because it is too large Load Diff