feat: add stats page

This commit is contained in:
Maël Gangloff
2024-08-22 19:26:34 +02:00
parent 6601540eb4
commit 26ae674117
10 changed files with 174 additions and 43 deletions

View File

@@ -4,10 +4,10 @@ import TextPage from "./pages/TextPage";
import DomainSearchPage from "./pages/search/DomainSearchPage";
import EntitySearchPage from "./pages/search/EntitySearchPage";
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
import TldPage from "./pages/info/TldPage";
import StatisticsPage from "./pages/info/StatisticsPage";
import TldPage from "./pages/search/TldPage";
import StatisticsPage from "./pages/StatisticsPage";
import WatchlistPage from "./pages/tracking/WatchlistPage";
import UserPage from "./pages/watchdog/UserPage";
import UserPage from "./pages/UserPage";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {getUser} from "./utils/api";
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
@@ -77,13 +77,12 @@ export default function App() {
<Route path="/search/domain" element={<DomainSearchPage/>}/>
<Route path="/search/entity" element={<EntitySearchPage/>}/>
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
<Route path="/info/tld" element={<TldPage/>}/>
<Route path="/info/stats" element={<StatisticsPage/>}/>
<Route path="/search/tld" element={<TldPage/>}/>
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
<Route path="/tracking/connectors" element={<ConnectorsPage/>}/>
<Route path="/stats" element={<StatisticsPage/>}/>
<Route path="/user" element={<UserPage/>}/>
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>

View File

@@ -48,7 +48,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
label: t`TLD`,
title: t`TLD list`,
disabled: !isAuthenticated,
onClick: () => navigate('/info/tld')
onClick: () => navigate('/search/tld')
},
{
key: 'entity-finder',
@@ -93,8 +93,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
key: 'stats',
icon: <LineChartOutlined/>,
label: t`Statistics`,
disabled: true,
onClick: () => navigate('/info/stats')
disabled: !isAuthenticated,
onClick: () => navigate('/stats')
}
]

View File

@@ -0,0 +1,117 @@
import React, {useEffect, useState} from "react";
import {getStatistics, Statistics} from "../utils/api";
import {Card, Col, Divider, Row, Statistic, Tooltip} from "antd";
import {t} from "ttag";
import {
AimOutlined,
CompassOutlined,
DatabaseOutlined,
FieldTimeOutlined,
NotificationOutlined
} from "@ant-design/icons";
export default function StatisticsPage() {
const [stats, setStats] = useState<Statistics>()
useEffect(() => {
getStatistics().then(setStats)
}, [])
const successRatio = stats !== undefined ?
(stats.domainPurchaseFailed === 0 ? undefined : stats.domainPurchased / stats.domainPurchaseFailed)
: undefined
return <>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
prefix={<CompassOutlined/>}
title={t`RDAP queries`}
value={stats?.rdapQueries}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Alert sent`}
prefix={<NotificationOutlined/>}
value={stats?.alertSent}
valueStyle={{color: 'blueviolet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain name in database`}
prefix={<DatabaseOutlined/>}
value={stats?.rdapQueries}
valueStyle={{color: 'darkblue'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain name tracked`}
prefix={<AimOutlined/>}
value={stats?.domainTracked}
valueStyle={{color: 'darkviolet'}}
/>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
<Col span={12}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={t`Domain name purchased`}
prefix={<FieldTimeOutlined/>}
value={stats?.domainPurchased}
valueStyle={{color: '#3f8600'}}
/>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<Tooltip
title={t`This value is based on the status code of the HTTP response from the providers following the order.`}>
<Statistic
loading={stats === undefined}
title={t`Success ratio`}
value={successRatio === undefined ? '-' : successRatio * 100}
suffix='%'
precision={2}
valueStyle={{color: successRatio === undefined ? 'black' : successRatio >= 0.5 ? 'darkgreen' : 'orange'}}
/>
</Tooltip>
</Card>
</Col>
</Row>
<Divider/>
<Row gutter={16}>
{stats?.domainCount.map(({domain, tld}) => <Col span={4}>
<Card bordered={false}>
<Statistic
loading={stats === undefined}
title={`.${tld}`}
value={domain}
valueStyle={{color: 'darkblue'}}
/>
</Card>
</Col>)}
</Row>
</>
}

View File

@@ -1,6 +1,6 @@
import React, {useEffect, useState} from "react";
import {Card, Flex, Skeleton, Typography} from "antd";
import {getUser, User} from "../../utils/api";
import {getUser, User} from "../utils/api";
import {t} from 'ttag'
export default function UserPage() {

View File

@@ -1,7 +0,0 @@
import React from "react";
export default function StatisticsPage() {
return <p>
Not implemented
</p>
}

View File

@@ -88,6 +88,16 @@ export interface InstanceConfig {
registerEnabled: boolean
}
export interface Statistics {
rdapQueries: number
alertSent: number
domainPurchased: number
domainPurchaseFailed: number
domainCount: {tld: string, domain: number}[]
domainCountTotal: number
domainTracked: number
}
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
const axiosConfig: AxiosRequestConfig = {
...config,

View File

@@ -1,4 +1,4 @@
import {InstanceConfig, request, User} from "./index";
import {InstanceConfig, request, Statistics, User} from "./index";
export async function login(email: string, password: string): Promise<boolean> {
@@ -32,4 +32,11 @@ export async function getConfiguration(): Promise<InstanceConfig> {
url: 'config'
})
return response.data
}
export async function getStatistics(): Promise<Statistics> {
const response = await request<Statistics>({
url: 'stats'
})
return response.data
}

View File

@@ -32,21 +32,26 @@ class StatisticsController extends AbstractController
->setDomainPurchased($this->pool->getItem('stats.domain.purchased')->get() ?? 0)
->setDomainPurchaseFailed($this->pool->getItem('stats.domain.purchase.failed')->get() ?? 0)
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
->setWatchlistCount(
$this->getCachedItem('stats.watchlist.count', fn () => $this->watchListRepository->count()
))
->setDomainTracked(
$this->watchListRepository->createQueryBuilder('w')
->join('w.domains', 'd')
->select('COUNT(DISTINCT d.ldhName)')
->where('d.deleted = FALSE')
->getQuery()->getSingleColumnResult()[0]
)
->setDomainCount(
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
->join('d.tld', 't')
->select('t.tld tld')
->addSelect('COUNT(d.ldhName) AS domain')
->addGroupBy('t.tld')
->where('d.deleted = FALSE')
->orderBy('domain', 'DESC')
->setMaxResults(5)
->getQuery()->getArrayResult())
)
->setDomainCountTotal(
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count()
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
));
return $stats;

View File

@@ -25,8 +25,21 @@ class Statistics
private ?int $domainPurchaseFailed = null;
private ?array $domainCount = null;
private ?int $domainTracked = null;
private ?int $domainCountTotal = null;
private ?int $watchlistCount = null;
public static function updateRDAPQueriesStat(CacheItemPoolInterface $pool, string $key): bool
{
try {
$item = $pool->getItem($key);
$item->set(($item->get() ?? 0) + 1);
return $pool->save($item);
} catch (\Throwable) {
}
return false;
}
public function getRdapQueries(): ?int
{
@@ -76,18 +89,6 @@ class Statistics
return $this;
}
public function getWatchlistCount(): ?int
{
return $this->watchlistCount;
}
public function setWatchlistCount(?int $watchlistCount): static
{
$this->watchlistCount = $watchlistCount;
return $this;
}
public function getDomainCountTotal(): ?int
{
return $this->domainCountTotal;
@@ -110,16 +111,15 @@ class Statistics
return $this;
}
public static function updateRDAPQueriesStat(CacheItemPoolInterface $pool, string $key): bool
public function getDomainTracked(): ?int
{
try {
$item = $pool->getItem($key);
$item->set(($item->get() ?? 0) + 1);
return $this->domainTracked;
}
return $pool->save($item);
} catch (\Throwable) {
}
public function setDomainTracked(?int $domainTracked): static
{
$this->domainTracked = $domainTracked;
return false;
return $this;
}
}