"use client"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient } from "@app/lib/api"; import { logAnalyticsFiltersSchema, logQueries, resourceQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { Card, CardContent, CardHeader } from "./ui/card"; import { LoaderIcon, RefreshCw, XIcon } from "lucide-react"; import { DateRangePicker, type DateTimeValue } from "./DateTimePicker"; import { Button } from "./ui/button"; import { cn } from "@app/lib/cn"; import { useTranslations } from "next-intl"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Label } from "./ui/label"; import { Separator } from "./ui/separator"; import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "./InfoSection"; import { WorldMap } from "./WorldMap"; import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji"; import { useTheme } from "next-themes"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from "./ui/chart"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; export type AnalyticsContentProps = { orgId: string; }; export function LogAnalyticsData(props: AnalyticsContentProps) { const searchParams = useSearchParams(); const path = usePathname(); const t = useTranslations(); const filters = logAnalyticsFiltersSchema.parse( Object.fromEntries(searchParams.entries()) ); const isEmptySearchParams = !filters.resourceId && !filters.timeStart && !filters.timeEnd; const env = useEnvContext(); const [api] = useState(() => createApiClient(env)); const router = useRouter(); const dateRange = { startDate: filters.timeStart ? new Date(filters.timeStart) : undefined, endDate: filters.timeEnd ? new Date(filters.timeEnd) : undefined }; const { data: resources = [], isFetching: isFetchingResources } = useQuery( resourceQueries.listNamesPerOrg(props.orgId, api) ); const { data: stats, isFetching: isFetchingAnalytics, refetch: refreshAnalytics, isLoading: isLoadingAnalytics // only `true` when there is no data yet } = useQuery( logQueries.requestAnalytics({ orgId: props.orgId, api, filters }) ); const percentBlocked = stats ? new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 2 }).format((stats.totalBlocked / stats.totalRequests) * 100) : null; const totalRequests = stats ? new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 0 }).format(stats.totalRequests) : null; function handleTimeRangeUpdate(start: DateTimeValue, end: DateTimeValue) { const newSearch = new URLSearchParams(searchParams); const timeRegex = /^(?\d{1,2})\:(?\d{1,2})(\:(?\d{1,2}))?$/; if (start.date) { const startDate = new Date(start.date); if (start.time) { const time = timeRegex.exec(start.time); const groups = time?.groups ?? {}; startDate.setHours(Number(groups.hours)); startDate.setMinutes(Number(groups.minutes)); if (groups.seconds) { startDate.setSeconds(Number(groups.seconds)); } } newSearch.set("timeStart", startDate.toISOString()); } if (end.date) { const endDate = new Date(end.date); if (end.time) { const time = timeRegex.exec(end.time); const groups = time?.groups ?? {}; endDate.setHours(Number(groups.hours)); endDate.setMinutes(Number(groups.minutes)); if (groups.seconds) { endDate.setSeconds(Number(groups.seconds)); } } console.log({ endDate }); newSearch.set("timeEnd", endDate.toISOString()); } router.replace(`${path}?${newSearch.toString()}`); } function getDateTime(date: Date) { return `${date.getHours()}:${date.getMinutes()}`; } return (
{!isEmptySearchParams && ( )}
{t("totalRequests")} {totalRequests ?? "--"} {t("totalBlocked")} {stats?.totalBlocked ?? "--"}  ( {percentBlocked ?? "--"} % )

{t("requestsByDay")}

{t("requestsByCountry")}

{t("topCountries")}

); } type RequestChartProps = { data: { day: string; allowedCount: number; blockedCount: number; totalCount: number; }[]; isLoading: boolean; }; function RequestChart(props: RequestChartProps) { const t = useTranslations(); const numberFormatter = new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 1, notation: "compact", compactDisplay: "short" }); const chartConfig = { day: { label: t("requestsByDay") }, blockedCount: { label: t("blocked"), color: "var(--chart-5)" }, allowedCount: { label: t("allowed"), color: "var(--chart-2)" } } satisfies ChartConfig; return ( } /> { const formattedDate = new Date( payload[0].payload.day ).toLocaleDateString(navigator.language, { dateStyle: "medium" }); return formattedDate; }} /> } /> datum.totalCount)) ]} allowDataOverflow type="number" tickFormatter={(value) => { return numberFormatter.format(value); }} /> { return new Date(value).toLocaleDateString( navigator.language, { dateStyle: "medium" } ); }} /> ); } type TopCountriesListProps = { countries: { code: string; count: number; }[]; total: number; isLoading: boolean; }; function TopCountriesList(props: TopCountriesListProps) { const t = useTranslations(); const displayNames = new Intl.DisplayNames(navigator.language, { type: "region", fallback: "code" }); const numberFormatter = new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 1, notation: "compact", compactDisplay: "short" }); const percentFormatter = new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 0, style: "percent" }); return (
{props.countries.length > 0 && (
{t("countries")}
{t("total")}
%
)} {/* `aspect-475/335` is the same aspect ratio as the world map component */}
    {props.countries.length === 0 && (
    {props.isLoading ? ( <> {" "} {t("loading")} ) : ( t("noData") )}
    )} {props.countries.map((country) => { const percent = country.count / props.total; return (
  1. {countryCodeToFlagEmoji(country.code)}{" "} {displayNames.of(country.code)}
    {Intl.NumberFormat( navigator.language ).format(country.count)} {" "} {country.count === 1 ? t("request") : t("requests")}
    {percentFormatter.format(percent)}
  2. ); })}
); }