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 {t} from "ttag"
import React, {useState} from "react" import React, {useEffect, useState} from "react"
import {Watchlist} from "../../pages/tracking/WatchlistPage"
import {ApartmentOutlined} from "@ant-design/icons" 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 [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 <> return <>
<Typography.Link> <Typography.Link>
<ApartmentOutlined title={t`View the Watchlist Entity Diagram`} <ApartmentOutlined title={t`View the Watchlist Entity Diagram`}
@ -17,13 +124,9 @@ export function ViewDiagramWatchlistButton({watchlist}: { watchlist: Watchlist }
title={t`Watchlist Entity Diagram`} title={t`Watchlist Entity Diagram`}
centered centered
open={open} open={open}
loading={loading}
footer={ footer={
<Space> <Space>
<Button type="primary" color='violet' onClick={() => {
}}>
Download
</Button>
<Button type="default" onClick={() => setOpen(false)}> <Button type="default" onClick={() => setOpen(false)}>
Close Close
</Button> </Button>
@ -31,10 +134,21 @@ export function ViewDiagramWatchlistButton({watchlist}: { watchlist: Watchlist }
} }
onOk={() => setOpen(false)} onOk={() => setOpen(false)}
onCancel={() => 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> </Modal>
</> </>
} }

View File

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

View File

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

View File

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

View File

@ -21,15 +21,18 @@
"@fontsource/noto-color-emoji": "^5.0.27", "@fontsource/noto-color-emoji": "^5.0.27",
"@symfony/webpack-encore": "^4.0.0", "@symfony/webpack-encore": "^4.0.0",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/dagre": "^0.7.52",
"@types/jsonld": "^1.5.15", "@types/jsonld": "^1.5.15",
"@types/punycode": "^2.1.4", "@types/punycode": "^2.1.4",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-responsive": "^8.0.8", "@types/react-responsive": "^8.0.8",
"@types/vcf": "^2.0.7", "@types/vcf": "^2.0.7",
"@xyflow/react": "^12.1.0",
"antd": "^5.19.3", "antd": "^5.19.3",
"axios": "^1.7.2", "axios": "^1.7.2",
"core-js": "^3.23.0", "core-js": "^3.23.0",
"dagre": "^0.8.5",
"html-loader": "^5.1.0", "html-loader": "^5.1.0",
"jsonld": "^8.3.2", "jsonld": "^8.3.2",
"punycode": "^2.3.1", "punycode": "^2.3.1",
@ -57,8 +60,5 @@
"build": "encore production --progress", "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: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" "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 "" msgstr ""
#: assets/components/tracking/WatchlistForm.tsx:100 #: assets/components/tracking/WatchlistForm.tsx:100
#: assets/components/tracking/WatchlistsList.tsx:28 #: assets/components/tracking/WatchlistsList.tsx:26
msgid "Domain names" msgid "Domain names"
msgstr "" msgstr ""
@ -162,7 +162,7 @@ msgid "Add a Domain name"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistForm.tsx:142 #: assets/components/tracking/WatchlistForm.tsx:142
#: assets/components/tracking/WatchlistsList.tsx:32 #: assets/components/tracking/WatchlistsList.tsx:30
msgid "Tracked events" msgid "Tracked events"
msgstr "" msgstr ""
@ -195,6 +195,14 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" 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 #: assets/components/tracking/ConnectorForm.tsx:40
msgid "Provider" msgid "Provider"
msgstr "" msgstr ""
@ -265,49 +273,49 @@ msgid ""
"the Provider's API" "the Provider's API"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:59 #: assets/components/tracking/UpdateWatchlistButton.tsx:31
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
msgid "Edit the Watchlist" msgid "Edit the Watchlist"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:86 #: assets/components/tracking/UpdateWatchlistButton.tsx:43
msgid "Update a Watchlist" msgid "Update a Watchlist"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:96 #: assets/components/tracking/UpdateWatchlistButton.tsx:53
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:110 #: assets/components/tracking/DeleteWatchlistButton.tsx:12
#: assets/components/tracking/WatchlistsList.tsx:117 #: assets/components/tracking/DeleteWatchlistButton.tsx:19
msgid "Delete the Watchlist" msgid "Delete the Watchlist"
msgstr "" msgstr ""
#: assets/components/tracking/WatchlistsList.tsx:111 #: assets/components/tracking/DeleteWatchlistButton.tsx:13
msgid "Are you sure to delete this Watchlist?" msgid "Are you sure to delete this Watchlist?"
msgstr "" msgstr ""
#: assets/components/tracking/ConnectorsList.tsx:25 #: assets/components/tracking/ConnectorsList.tsx:25
#: assets/components/tracking/WatchlistsList.tsx:113 #: assets/components/tracking/DeleteWatchlistButton.tsx:15
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: assets/components/tracking/ConnectorsList.tsx:26 #: assets/components/tracking/ConnectorsList.tsx:26
#: assets/components/tracking/WatchlistsList.tsx:114 #: assets/components/tracking/DeleteWatchlistButton.tsx:16
msgid "No" msgid "No"
msgstr "" 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 #: assets/components/tracking/ConnectorsList.tsx:19
#, javascript-format #, javascript-format
msgid "Connector ${ connector.provider }" msgid "Connector ${ connector.provider }"

695
yarn.lock

File diff suppressed because it is too large Load Diff