mirror of
https://github.com/pommee/goaway.git
synced 2026-05-20 00:09:11 -05:00
fix: improve response ip and rtype, better ip view for logs, requires regeneration of database
This commit is contained in:
@@ -133,19 +133,25 @@ func NewSourcesTable(db *sql.DB) error {
|
||||
func NewRequestLogTable(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS request_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
blocked BOOLEAN NOT NULL,
|
||||
cached BOOLEAN NOT NULL,
|
||||
response_time_ns INTEGER NOT NULL,
|
||||
client_ip TEXT,
|
||||
client_name TEXT,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
blocked BOOLEAN NOT NULL,
|
||||
cached BOOLEAN NOT NULL,
|
||||
response_time_ns INTEGER NOT NULL,
|
||||
client_ip TEXT,
|
||||
client_name TEXT,
|
||||
status TEXT,
|
||||
query_type TEXT,
|
||||
response_size_bytes TEXT
|
||||
);
|
||||
response_size_bytes INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS request_log_ips (
|
||||
id SERIAL PRIMARY KEY,
|
||||
request_log_id INTEGER REFERENCES request_log(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
rtype TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -114,11 +114,16 @@ func GetUniqueQueryTypes(db *sql.DB) ([]interface{}, error) {
|
||||
|
||||
func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, error) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT timestamp, domain, ip, blocked, cached, response_time_ns, client_ip, client_name, status, query_type, response_size_bytes
|
||||
FROM request_log
|
||||
WHERE domain LIKE ?
|
||||
SELECT rl.timestamp, rl.domain, rl.blocked, rl.cached, rl.response_time_ns,
|
||||
rl.client_ip, rl.client_name, rl.status, rl.query_type, rl.response_size_bytes,
|
||||
COALESCE(STRING_AGG(rli.ip || ':' || rli.rtype, ','), '') as resolved_ips
|
||||
FROM request_log rl
|
||||
LEFT JOIN request_log_ips rli ON rl.id = rli.request_log_id
|
||||
WHERE rl.domain LIKE $1
|
||||
GROUP BY rl.id, rl.timestamp, rl.domain, rl.blocked, rl.cached, rl.response_time_ns,
|
||||
rl.client_ip, rl.client_name, rl.status, rl.query_type, rl.response_size_bytes
|
||||
ORDER BY %s %s
|
||||
LIMIT ? OFFSET ?`, q.Column, q.Direction)
|
||||
LIMIT $2 OFFSET $3`, q.Column, q.Direction)
|
||||
|
||||
rows, err := db.Query(query, "%"+q.Search+"%", q.PageSize, q.Offset)
|
||||
if err != nil {
|
||||
@@ -132,14 +137,14 @@ func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, er
|
||||
var queries []model.RequestLogEntry
|
||||
for rows.Next() {
|
||||
var query model.RequestLogEntry
|
||||
var ipString string
|
||||
var resolvedIPsString string
|
||||
var timestamp string
|
||||
query.ClientInfo = &model.Client{}
|
||||
|
||||
if err := rows.Scan(
|
||||
×tamp, &query.Domain, &ipString,
|
||||
&query.Blocked, &query.Cached, &query.ResponseTime,
|
||||
&query.ClientInfo.IP, &query.ClientInfo.Name, &query.Status, &query.QueryType, &query.ResponseSizeBytes,
|
||||
×tamp, &query.Domain, &query.Blocked, &query.Cached, &query.ResponseTime,
|
||||
&query.ClientInfo.IP, &query.ClientInfo.Name, &query.Status, &query.QueryType,
|
||||
&query.ResponseSizeBytes, &resolvedIPsString,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,13 +154,37 @@ func FetchQueries(db *sql.DB, q models.QueryParams) ([]model.RequestLogEntry, er
|
||||
return nil, fmt.Errorf("error: %v", err)
|
||||
}
|
||||
query.Timestamp = time.Unix(parsedTime, 0)
|
||||
query.IP = strings.Split(ipString, ",")
|
||||
query.IP = parseResolvedIPs(resolvedIPsString)
|
||||
queries = append(queries, query)
|
||||
}
|
||||
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
func parseResolvedIPs(ipString string) []model.ResolvedIP {
|
||||
if ipString == "" {
|
||||
return []model.ResolvedIP{}
|
||||
}
|
||||
|
||||
parts := strings.Split(ipString, ",")
|
||||
resolvedIPs := make([]model.ResolvedIP, 0, len(parts))
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
ipParts := strings.Split(part, ":")
|
||||
if len(ipParts) == 2 {
|
||||
resolvedIPs = append(resolvedIPs, model.ResolvedIP{
|
||||
IP: ipParts[0],
|
||||
RType: ipParts[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedIPs
|
||||
}
|
||||
|
||||
func FetchAllClients(db *sql.DB) (map[string]dbModel.Client, error) {
|
||||
uniqueClients := make(map[string]dbModel.Client)
|
||||
|
||||
@@ -364,7 +393,6 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
_ = tx.Rollback()
|
||||
@@ -376,15 +404,15 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
|
||||
}
|
||||
}()
|
||||
|
||||
valueStrings := make([]string, 0, len(entries))
|
||||
valueArgs := make([]interface{}, 0, len(entries)*11)
|
||||
|
||||
for _, entry := range entries {
|
||||
valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
valueArgs = append(valueArgs,
|
||||
var logID int64
|
||||
err = tx.QueryRow(`
|
||||
INSERT INTO request_log (timestamp, domain, blocked, cached, response_time_ns,
|
||||
client_ip, client_name, status, query_type, response_size_bytes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id`,
|
||||
entry.Timestamp.Unix(),
|
||||
entry.Domain,
|
||||
strings.Join(entry.IP, ","),
|
||||
entry.Blocked,
|
||||
entry.Cached,
|
||||
entry.ResponseTime,
|
||||
@@ -393,15 +421,22 @@ func SaveRequestLog(db *sql.DB, entries []model.RequestLogEntry) error {
|
||||
entry.Status,
|
||||
entry.QueryType,
|
||||
entry.ResponseSizeBytes,
|
||||
)
|
||||
}
|
||||
).Scan(&logID)
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO request_log (timestamp, domain, ip, blocked, cached, response_time_ns, client_ip, client_name, status, query_type, response_size_bytes) VALUES %s",
|
||||
strings.Join(valueStrings, ","))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save request log: %v", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(query, valueArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save request log. Reason: %v", err)
|
||||
for _, resolvedIP := range entry.IP {
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO request_log_ips (request_log_id, ip, rtype)
|
||||
VALUES ($1, $2, $3)`,
|
||||
logID, resolvedIP.IP, resolvedIP.RType)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save resolved IP: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -18,9 +18,6 @@ import (
|
||||
var (
|
||||
blackholeIPv4 = net.ParseIP("0.0.0.0")
|
||||
blackholeIPv6 = net.ParseIP("::")
|
||||
|
||||
resolvedIPv4 = []string{"0.0.0.0"}
|
||||
resolvedIPv6 = []string{"::"}
|
||||
)
|
||||
|
||||
func trimDomainDot(name string) string {
|
||||
@@ -41,6 +38,10 @@ func (s *DNSServer) processQuery(request *Request) model.RequestLogEntry {
|
||||
s.Config.DNS.Status.Paused = false
|
||||
}
|
||||
|
||||
// TODO
|
||||
// IsBlacklisted will not work if the trailing dot is removed
|
||||
// Trailing dot shall exist to be fully quallyfied
|
||||
// However many blacklisted domains miss the trailing dot
|
||||
if !s.Config.DNS.Status.Paused && s.Blacklist.IsBlacklisted(domainName) && !s.Whitelist.IsWhitelisted(domainName) {
|
||||
return s.handleBlacklisted(request)
|
||||
}
|
||||
@@ -187,10 +188,15 @@ func (s *DNSServer) respondWithLocalhost(request *Request) model.RequestLogEntry
|
||||
_ = request.W.WriteMsg(request.Msg)
|
||||
|
||||
return model.RequestLogEntry{
|
||||
Timestamp: request.Sent,
|
||||
Domain: request.Question.Name,
|
||||
Status: dns.RcodeToString[dns.RcodeSuccess],
|
||||
IP: []string{"localhost.lan"},
|
||||
Timestamp: request.Sent,
|
||||
Domain: request.Question.Name,
|
||||
Status: dns.RcodeToString[dns.RcodeSuccess],
|
||||
IP: []model.ResolvedIP{
|
||||
{
|
||||
IP: "localhost.lan",
|
||||
RType: "PTR",
|
||||
},
|
||||
},
|
||||
Blocked: false,
|
||||
Cached: false,
|
||||
ResponseTime: time.Since(request.Sent),
|
||||
@@ -220,10 +226,15 @@ func (s *DNSServer) respondWithHostname(request *Request, hostname string) model
|
||||
_ = request.W.WriteMsg(request.Msg)
|
||||
|
||||
return model.RequestLogEntry{
|
||||
Domain: request.Question.Name,
|
||||
Status: dns.RcodeToString[dns.RcodeSuccess],
|
||||
QueryType: dns.TypeToString[request.Question.Qtype],
|
||||
IP: []string{hostname},
|
||||
Domain: request.Question.Name,
|
||||
Status: dns.RcodeToString[dns.RcodeSuccess],
|
||||
QueryType: dns.TypeToString[request.Question.Qtype],
|
||||
IP: []model.ResolvedIP{
|
||||
{
|
||||
IP: hostname,
|
||||
RType: "PTR",
|
||||
},
|
||||
},
|
||||
ResponseSizeBytes: request.Msg.Len(),
|
||||
Timestamp: request.Sent,
|
||||
ResponseTime: time.Since(request.Sent),
|
||||
@@ -247,10 +258,13 @@ func (s *DNSServer) forwardPTRQueryUpstream(request *Request) model.RequestLogEn
|
||||
request.Msg.Authoritative = false
|
||||
request.Msg.RecursionAvailable = true
|
||||
|
||||
var resolvedHostnames []string
|
||||
var resolvedHostnames []model.ResolvedIP
|
||||
for _, answer := range answers {
|
||||
if ptr, ok := answer.(*dns.PTR); ok {
|
||||
resolvedHostnames = append(resolvedHostnames, ptr.Ptr)
|
||||
resolvedHostnames = append(resolvedHostnames, model.ResolvedIP{
|
||||
IP: ptr.Ptr,
|
||||
RType: "PTR",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +285,7 @@ func (s *DNSServer) forwardPTRQueryUpstream(request *Request) model.RequestLogEn
|
||||
func (s *DNSServer) handleStandardQuery(req *Request) model.RequestLogEntry {
|
||||
answers, cached, status := s.Resolve(req)
|
||||
|
||||
resolved := make([]string, 0, len(answers))
|
||||
resolved := make([]model.ResolvedIP, 0, len(answers))
|
||||
req.Msg.Answer = answers
|
||||
|
||||
if req.Msg.RecursionDesired {
|
||||
@@ -281,13 +295,25 @@ func (s *DNSServer) handleStandardQuery(req *Request) model.RequestLogEntry {
|
||||
for _, a := range answers {
|
||||
switch rr := a.(type) {
|
||||
case *dns.A:
|
||||
resolved = append(resolved, rr.A.String())
|
||||
resolved = append(resolved, model.ResolvedIP{
|
||||
IP: rr.A.String(),
|
||||
RType: "A",
|
||||
})
|
||||
case *dns.AAAA:
|
||||
resolved = append(resolved, rr.AAAA.String())
|
||||
resolved = append(resolved, model.ResolvedIP{
|
||||
IP: rr.AAAA.String(),
|
||||
RType: "AAAA",
|
||||
})
|
||||
case *dns.PTR:
|
||||
resolved = append(resolved, rr.Ptr)
|
||||
resolved = append(resolved, model.ResolvedIP{
|
||||
IP: rr.Ptr,
|
||||
RType: "PTR",
|
||||
})
|
||||
case *dns.CNAME:
|
||||
resolved = append(resolved, rr.Target)
|
||||
resolved = append(resolved, model.ResolvedIP{
|
||||
IP: rr.Target,
|
||||
RType: "CNAME",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +508,7 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
|
||||
request.Msg.RecursionAvailable = true
|
||||
request.Msg.Rcode = dns.RcodeSuccess
|
||||
|
||||
var resolved []string
|
||||
var resolved []model.ResolvedIP
|
||||
cacheTTL := uint32(s.Config.DNS.CacheTTL)
|
||||
|
||||
switch request.Question.Qtype {
|
||||
@@ -514,7 +540,12 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
|
||||
A: blackholeIPv4,
|
||||
}}
|
||||
}
|
||||
resolved = resolvedIPv4
|
||||
resolved = []model.ResolvedIP{
|
||||
{
|
||||
IP: blackholeIPv4.String(),
|
||||
RType: "A",
|
||||
},
|
||||
}
|
||||
|
||||
case dns.TypeAAAA:
|
||||
if len(request.Msg.Answer) == 1 {
|
||||
@@ -544,7 +575,12 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
|
||||
AAAA: blackholeIPv6,
|
||||
}}
|
||||
}
|
||||
resolved = resolvedIPv6
|
||||
resolved = []model.ResolvedIP{
|
||||
{
|
||||
IP: blackholeIPv6.String(),
|
||||
RType: "AAAA",
|
||||
},
|
||||
}
|
||||
|
||||
default:
|
||||
request.Msg.Rcode = dns.RcodeNameError
|
||||
@@ -567,6 +603,7 @@ func (s *DNSServer) handleBlacklisted(request *Request) model.RequestLogEntry {
|
||||
Timestamp: request.Sent,
|
||||
ResponseTime: time.Since(request.Sent),
|
||||
Blocked: true,
|
||||
Cached: false,
|
||||
ClientInfo: request.Client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ type RequestLogEntry struct {
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
QueryType string `json:"queryType"`
|
||||
IP []string `json:"ip"`
|
||||
IP []ResolvedIP `json:"ip"`
|
||||
ResponseSizeBytes int `json:"responseSizeBytes"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ResponseTime time.Duration `json:"responseTimeNS"`
|
||||
@@ -15,6 +15,11 @@ type RequestLogEntry struct {
|
||||
ClientInfo *Client `json:"client"`
|
||||
}
|
||||
|
||||
type ResolvedIP struct {
|
||||
IP string `json:"ip"`
|
||||
RType string `json:"rtype"`
|
||||
}
|
||||
|
||||
type RequestLogIntervalSummary struct {
|
||||
IntervalStart time.Time `json:"start"`
|
||||
BlockedCount int `json:"blocked"`
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Check, Lightning, ShieldSlash } from "@phosphor-icons/react";
|
||||
import { IPEntry } from "@/pages/logs";
|
||||
import { DeleteRequest, GetRequest, PostRequest } from "@/util";
|
||||
import {
|
||||
Check,
|
||||
CheckIcon,
|
||||
Lightning,
|
||||
ShieldSlash,
|
||||
TrashIcon
|
||||
} from "@phosphor-icons/react";
|
||||
import { Checkbox } from "@radix-ui/react-checkbox";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
@@ -13,7 +21,7 @@ export type Queries = {
|
||||
cached: boolean;
|
||||
client: Client;
|
||||
domain: string;
|
||||
ip: Array<string>;
|
||||
ip: IPEntry[];
|
||||
queryType: string;
|
||||
responseTimeNS: number;
|
||||
status: string;
|
||||
@@ -74,8 +82,30 @@ export const columns: ColumnDef<Queries>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "ip",
|
||||
header: "IP",
|
||||
cell: ({ row }) => <div>{row.getValue("ip")}</div>
|
||||
header: "IP(s)",
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue() as IPEntry[];
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{value.map((entry, i) => {
|
||||
if (entry && typeof entry === "object" && entry.ip) {
|
||||
const ip = String(entry.ip || "");
|
||||
const rtype = String(entry.rtype || "");
|
||||
return (
|
||||
<span key={i}>
|
||||
{ip} {rtype && `(${rtype})`}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return <span key={i}>{String(entry || "")}</span>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div>{String(value || "")}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "client",
|
||||
|
||||
@@ -395,8 +395,8 @@ docker pull pommee/goaway:version`;
|
||||
log.includes("[info]")
|
||||
? "text-green-400"
|
||||
: log.includes("[error]")
|
||||
? "text-red-400"
|
||||
: ""
|
||||
? "text-red-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{log}
|
||||
|
||||
+107
-29
@@ -59,12 +59,36 @@ import {
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type QueryResponse = {
|
||||
details: Queries[];
|
||||
interface Client {
|
||||
ip: string;
|
||||
name: string;
|
||||
mac: string;
|
||||
}
|
||||
|
||||
export interface IPEntry {
|
||||
ip: string;
|
||||
rtype: string;
|
||||
}
|
||||
|
||||
interface QueryDetail {
|
||||
domain: string;
|
||||
status: string;
|
||||
queryType: string;
|
||||
ip: IPEntry[];
|
||||
responseSizeBytes: number;
|
||||
timestamp: string;
|
||||
responseTimeNS: number;
|
||||
blocked: boolean;
|
||||
cached: boolean;
|
||||
client: Client;
|
||||
}
|
||||
|
||||
interface QueryResponse {
|
||||
details: QueryDetail[];
|
||||
draw: string;
|
||||
recordsFiltered: number;
|
||||
recordsTotal: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchQueries(
|
||||
page: number,
|
||||
@@ -83,8 +107,9 @@ async function fetchQueries(
|
||||
return {
|
||||
details: response.details.map(
|
||||
(item: {
|
||||
client: { ip: string; name: string; mac: string };
|
||||
ip: string;
|
||||
client: { ip?: string; name?: string; mac?: string };
|
||||
ip?: { ip?: string; rtype?: string }[];
|
||||
[key: string]: string;
|
||||
}) => ({
|
||||
...item,
|
||||
client: {
|
||||
@@ -92,7 +117,12 @@ async function fetchQueries(
|
||||
name: item.client?.name || "",
|
||||
mac: item.client?.mac || ""
|
||||
},
|
||||
ip: Array.isArray(item.ip) ? item.ip : []
|
||||
ip: Array.isArray(item.ip)
|
||||
? item.ip.map((entry) => ({
|
||||
ip: String(entry?.ip || ""),
|
||||
rtype: String(entry?.rtype || "")
|
||||
}))
|
||||
: []
|
||||
})
|
||||
),
|
||||
draw: response.draw || "1",
|
||||
@@ -127,9 +157,9 @@ export function Logs() {
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const totalPages = Math.ceil(totalRecords / pageSize);
|
||||
|
||||
const debounce = (func: { (value) }, delay: number | undefined) => {
|
||||
let timeoutId: string | number | NodeJS.Timeout | undefined;
|
||||
return (...args) => {
|
||||
const debounce = (func: (...args: unknown[]) => void, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
return (...args: unknown[]) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args);
|
||||
@@ -139,7 +169,7 @@ export function Logs() {
|
||||
|
||||
const debouncedSetDomainFilter = useMemo(
|
||||
() =>
|
||||
debounce((value) => {
|
||||
debounce((value: string) => {
|
||||
setDomainFilter(value);
|
||||
setPageIndex(0);
|
||||
}, 500),
|
||||
@@ -159,14 +189,19 @@ export function Logs() {
|
||||
try {
|
||||
const newQuery = JSON.parse(event.data);
|
||||
|
||||
const formattedQuery = {
|
||||
const formattedQuery: Queries = {
|
||||
...newQuery,
|
||||
client: {
|
||||
ip: newQuery.client?.ip || "",
|
||||
name: newQuery.client?.name || "",
|
||||
mac: newQuery.client?.mac || ""
|
||||
},
|
||||
ip: Array.isArray(newQuery.ip) ? newQuery.ip : []
|
||||
ip: Array.isArray(newQuery.ip)
|
||||
? newQuery.ip.map((entry: IPEntry) => ({
|
||||
ip: String(entry?.ip || ""),
|
||||
rtype: String(entry?.rtype || "")
|
||||
}))
|
||||
: []
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -189,16 +224,16 @@ export function Logs() {
|
||||
} catch (error) {
|
||||
console.error("Error handling WebSocket message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
setWsConnected(false);
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
setWsConnected(false);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket connection closed");
|
||||
setWsConnected(false);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket connection closed");
|
||||
setWsConnected(false);
|
||||
};
|
||||
|
||||
return () => {
|
||||
@@ -394,17 +429,60 @@ export function Logs() {
|
||||
}}
|
||||
className="block truncate"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
{(() => {
|
||||
if (cell.column.id === "ip") {
|
||||
const ipValue = cell.getValue() as IPEntry[];
|
||||
if (
|
||||
Array.isArray(ipValue) &&
|
||||
ipValue.length > 0
|
||||
) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{ipValue[0]?.ip || ""}</span>
|
||||
{ipValue.length > 1 && (
|
||||
<span className="text-xs text-stone-400 border-1 ml-1 px-1 rounded border-green-600/60">
|
||||
+{ipValue.length - 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
);
|
||||
})()}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-stone-800 border-1 border-stone-700 text-white p-3">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
<TooltipContent className="bg-stone-800 border border-stone-700 text-white text-sm p-3 rounded-md shadow-md font-mono">
|
||||
{(() => {
|
||||
if (cell.column.id === "ip") {
|
||||
const ipValue = cell.getValue() as IPEntry[];
|
||||
return Array.isArray(ipValue) ? (
|
||||
<div className="space-y-1">
|
||||
{ipValue.map((entry, i) => (
|
||||
<div key={i} className="flex gap-2">
|
||||
<span className="inline-block w-[80px] text-stone-400">
|
||||
{entry?.rtype
|
||||
? `[${entry.rtype}]`
|
||||
: ""}
|
||||
</span>
|
||||
<span>{entry?.ip || ""}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
return flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
);
|
||||
})()}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
Reference in New Issue
Block a user