mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: add stats page
This commit is contained in:
@@ -4,10 +4,10 @@ import TextPage from "./pages/TextPage";
|
|||||||
import DomainSearchPage from "./pages/search/DomainSearchPage";
|
import DomainSearchPage from "./pages/search/DomainSearchPage";
|
||||||
import EntitySearchPage from "./pages/search/EntitySearchPage";
|
import EntitySearchPage from "./pages/search/EntitySearchPage";
|
||||||
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
|
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
|
||||||
import TldPage from "./pages/info/TldPage";
|
import TldPage from "./pages/search/TldPage";
|
||||||
import StatisticsPage from "./pages/info/StatisticsPage";
|
import StatisticsPage from "./pages/StatisticsPage";
|
||||||
import WatchlistPage from "./pages/tracking/WatchlistPage";
|
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 React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
import {getUser} from "./utils/api";
|
import {getUser} from "./utils/api";
|
||||||
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
|
import LoginPage, {AuthenticatedContext} from "./pages/LoginPage";
|
||||||
@@ -77,13 +77,12 @@ export default function App() {
|
|||||||
<Route path="/search/domain" element={<DomainSearchPage/>}/>
|
<Route path="/search/domain" element={<DomainSearchPage/>}/>
|
||||||
<Route path="/search/entity" element={<EntitySearchPage/>}/>
|
<Route path="/search/entity" element={<EntitySearchPage/>}/>
|
||||||
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
|
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
|
||||||
|
<Route path="/search/tld" element={<TldPage/>}/>
|
||||||
<Route path="/info/tld" element={<TldPage/>}/>
|
|
||||||
<Route path="/info/stats" element={<StatisticsPage/>}/>
|
|
||||||
|
|
||||||
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
|
<Route path="/tracking/watchlist" element={<WatchlistPage/>}/>
|
||||||
<Route path="/tracking/connectors" element={<ConnectorsPage/>}/>
|
<Route path="/tracking/connectors" element={<ConnectorsPage/>}/>
|
||||||
|
|
||||||
|
<Route path="/stats" element={<StatisticsPage/>}/>
|
||||||
<Route path="/user" element={<UserPage/>}/>
|
<Route path="/user" element={<UserPage/>}/>
|
||||||
|
|
||||||
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
|
<Route path="/faq" element={<TextPage resource='faq.md'/>}/>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
|||||||
label: t`TLD`,
|
label: t`TLD`,
|
||||||
title: t`TLD list`,
|
title: t`TLD list`,
|
||||||
disabled: !isAuthenticated,
|
disabled: !isAuthenticated,
|
||||||
onClick: () => navigate('/info/tld')
|
onClick: () => navigate('/search/tld')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'entity-finder',
|
key: 'entity-finder',
|
||||||
@@ -93,8 +93,8 @@ export function Sider({isAuthenticated}: { isAuthenticated: boolean }) {
|
|||||||
key: 'stats',
|
key: 'stats',
|
||||||
icon: <LineChartOutlined/>,
|
icon: <LineChartOutlined/>,
|
||||||
label: t`Statistics`,
|
label: t`Statistics`,
|
||||||
disabled: true,
|
disabled: !isAuthenticated,
|
||||||
onClick: () => navigate('/info/stats')
|
onClick: () => navigate('/stats')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
117
assets/pages/StatisticsPage.tsx
Normal file
117
assets/pages/StatisticsPage.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {Card, Flex, Skeleton, Typography} from "antd";
|
import {Card, Flex, Skeleton, Typography} from "antd";
|
||||||
import {getUser, User} from "../../utils/api";
|
import {getUser, User} from "../utils/api";
|
||||||
import {t} from 'ttag'
|
import {t} from 'ttag'
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function StatisticsPage() {
|
|
||||||
return <p>
|
|
||||||
Not implemented
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
@@ -88,6 +88,16 @@ export interface InstanceConfig {
|
|||||||
registerEnabled: boolean
|
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> {
|
export async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
|
||||||
const axiosConfig: AxiosRequestConfig = {
|
const axiosConfig: AxiosRequestConfig = {
|
||||||
...config,
|
...config,
|
||||||
|
|||||||
@@ -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> {
|
export async function login(email: string, password: string): Promise<boolean> {
|
||||||
@@ -32,4 +32,11 @@ export async function getConfiguration(): Promise<InstanceConfig> {
|
|||||||
url: 'config'
|
url: 'config'
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStatistics(): Promise<Statistics> {
|
||||||
|
const response = await request<Statistics>({
|
||||||
|
url: 'stats'
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,26 @@ class StatisticsController extends AbstractController
|
|||||||
->setDomainPurchased($this->pool->getItem('stats.domain.purchased')->get() ?? 0)
|
->setDomainPurchased($this->pool->getItem('stats.domain.purchased')->get() ?? 0)
|
||||||
->setDomainPurchaseFailed($this->pool->getItem('stats.domain.purchase.failed')->get() ?? 0)
|
->setDomainPurchaseFailed($this->pool->getItem('stats.domain.purchase.failed')->get() ?? 0)
|
||||||
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
|
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
|
||||||
->setWatchlistCount(
|
->setDomainTracked(
|
||||||
$this->getCachedItem('stats.watchlist.count', fn () => $this->watchListRepository->count()
|
$this->watchListRepository->createQueryBuilder('w')
|
||||||
))
|
->join('w.domains', 'd')
|
||||||
|
->select('COUNT(DISTINCT d.ldhName)')
|
||||||
|
->where('d.deleted = FALSE')
|
||||||
|
->getQuery()->getSingleColumnResult()[0]
|
||||||
|
)
|
||||||
->setDomainCount(
|
->setDomainCount(
|
||||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
|
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
|
||||||
->join('d.tld', 't')
|
->join('d.tld', 't')
|
||||||
->select('t.tld tld')
|
->select('t.tld tld')
|
||||||
->addSelect('COUNT(d.ldhName) AS domain')
|
->addSelect('COUNT(d.ldhName) AS domain')
|
||||||
->addGroupBy('t.tld')
|
->addGroupBy('t.tld')
|
||||||
|
->where('d.deleted = FALSE')
|
||||||
->orderBy('domain', 'DESC')
|
->orderBy('domain', 'DESC')
|
||||||
->setMaxResults(5)
|
->setMaxResults(5)
|
||||||
->getQuery()->getArrayResult())
|
->getQuery()->getArrayResult())
|
||||||
)
|
)
|
||||||
->setDomainCountTotal(
|
->setDomainCountTotal(
|
||||||
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count()
|
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
|
||||||
));
|
));
|
||||||
|
|
||||||
return $stats;
|
return $stats;
|
||||||
|
|||||||
@@ -25,8 +25,21 @@ class Statistics
|
|||||||
private ?int $domainPurchaseFailed = null;
|
private ?int $domainPurchaseFailed = null;
|
||||||
|
|
||||||
private ?array $domainCount = null;
|
private ?array $domainCount = null;
|
||||||
|
private ?int $domainTracked = null;
|
||||||
private ?int $domainCountTotal = 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
|
public function getRdapQueries(): ?int
|
||||||
{
|
{
|
||||||
@@ -76,18 +89,6 @@ class Statistics
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWatchlistCount(): ?int
|
|
||||||
{
|
|
||||||
return $this->watchlistCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setWatchlistCount(?int $watchlistCount): static
|
|
||||||
{
|
|
||||||
$this->watchlistCount = $watchlistCount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDomainCountTotal(): ?int
|
public function getDomainCountTotal(): ?int
|
||||||
{
|
{
|
||||||
return $this->domainCountTotal;
|
return $this->domainCountTotal;
|
||||||
@@ -110,16 +111,15 @@ class Statistics
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function updateRDAPQueriesStat(CacheItemPoolInterface $pool, string $key): bool
|
public function getDomainTracked(): ?int
|
||||||
{
|
{
|
||||||
try {
|
return $this->domainTracked;
|
||||||
$item = $pool->getItem($key);
|
}
|
||||||
$item->set(($item->get() ?? 0) + 1);
|
|
||||||
|
|
||||||
return $pool->save($item);
|
public function setDomainTracked(?int $domainTracked): static
|
||||||
} catch (\Throwable) {
|
{
|
||||||
}
|
$this->domainTracked = $domainTracked;
|
||||||
|
|
||||||
return false;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user