"use client"; import { useMemo } from "react"; import type { BoxProps } from "@mantine/core"; import { Avatar, AvatarGroup, Card, Flex, SimpleGrid, Stack, Text, Tooltip, TooltipFloating } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { formatNumber } from "@homarr/common"; import { integrationDefs } from "@homarr/definitions"; import type { DnsHoleSummary } from "@homarr/integrations/types"; import type { stringOrTranslation, TranslationFunction } from "@homarr/translation"; import { translateIfNecessary } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { TablerIcon } from "@homarr/ui"; import type { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps) { const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( { integrationIds, }, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, }, ); const utils = clientApi.useUtils(); const t = useI18n(); clientApi.widget.dnsHole.subscribeToSummary.useSubscription( { integrationIds, }, { onData: (data) => { utils.widget.dnsHole.summary.setData( { integrationIds, }, (prevData) => { if (!prevData) { return undefined; } const newData = prevData.map((item) => item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, ); return newData; }, ); }, }, ); const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( {data.length > 0 ? ( stats.map((item) => ( )) ) : ( {summaries.map(({ integration }) => ( ))} {t("widget.dnsHoleSummary.error.integrationsDisconnected")} )} ); } const stats = [ { icon: IconBarrierBlock, value: (data, size) => formatNumber( data.reduce((count, { adsBlockedToday }) => count + adsBlockedToday, 0), size === "sm" ? 0 : 2, ), label: (t) => t("widget.dnsHoleSummary.data.adsBlockedToday"), color: "rgba(240, 82, 60, 0.4)", // RED }, { icon: IconPercentage, value: (data, size) => { const totalCount = data.reduce((count, { dnsQueriesToday }) => count + dnsQueriesToday, 0); const blocked = data.reduce((count, { adsBlockedToday }) => count + adsBlockedToday, 0); return `${formatNumber(totalCount === 0 ? 0 : (blocked / totalCount) * 100, size === "sm" ? 0 : 2)}%`; }, label: (t) => t("widget.dnsHoleSummary.data.adsBlockedTodayPercentage"), color: "rgba(255, 165, 20, 0.4)", // YELLOW }, { icon: IconSearch, value: (data, size) => formatNumber( data.reduce((count, { dnsQueriesToday }) => count + dnsQueriesToday, 0), size === "sm" ? 0 : 2, ), label: (t) => t("widget.dnsHoleSummary.data.dnsQueriesToday"), color: "rgba(0, 175, 218, 0.4)", // BLUE }, { icon: IconWorldWww, value: (data, size) => { // We use a suffix to indicate that there might be more domains in the at least two lists. const suffix = data.length >= 2 ? "+" : ""; return ( formatNumber( data.reduce((count, { domainsBeingBlocked }) => count + domainsBeingBlocked, 0), size === "sm" ? 0 : 2, ) + suffix ); }, tooltip: (data, t) => (data.length >= 2 ? t("widget.dnsHoleSummary.domainsTooltip") : undefined), label: (t) => t("widget.dnsHoleSummary.data.domainsBeingBlocked"), color: "rgba(0, 176, 96, 0.4)", // GREEN }, ] satisfies StatItem[]; interface StatItem { icon: TablerIcon; value: (summaries: DnsHoleSummary[], size: "sm" | "md") => string; tooltip?: (summaries: DnsHoleSummary[], t: TranslationFunction) => string | undefined; label: stringOrTranslation; color: string; } interface StatCardProps { item: StatItem; data: DnsHoleSummary[]; usePiHoleColors: boolean; t: TranslationFunction; } const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => { const { ref, height, width } = useElementSize(); const isLong = width > height + 20; const canStackText = height > 32; const hideLabel = (height <= 32 && width <= 256) || (height <= 64 && width <= 92); const tooltip = item.tooltip?.(data, t); const board = useRequiredBoard(); return ( {item.value(data, width <= 64 ? "sm" : "md")} {!hideLabel && ( {translateIfNecessary(t, item.label)} )} ); }; const boxPropsByLayout = (layout: WidgetProps<"dnsHoleSummary">["options"]["layout"]): BoxProps => { if (layout === "grid") { return { display: "grid", style: { gridTemplateColumns: "1fr 1fr", gridTemplateRows: "1fr 1fr", }, }; } return { display: "flex", style: { flexDirection: layout, }, }; };