"use client"; import type { BoxProps } from "@mantine/core"; import { Avatar, AvatarGroup, Badge, Box, Card, Flex, Group, ScrollArea, SimpleGrid, Stack, Text, Tooltip, } 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 { zoomCompensatedSize } from "@homarr/ui"; import type { TablerIcon } from "@homarr/ui"; import type { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; import { IntegrationErrorIndicator } from "../../common/integration-error-indicator"; import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../../common/query-state"; import { WidgetQueryLoadingState } from "../../common/query-state-indicator"; export default function DnsHoleSummaryWidget({ options, integrationIds, width, height, }: WidgetComponentProps) { const summaryQuery = clientApi.widget.dnsHole.summary.useQuery({ integrationIds, }); const summaries = getUsableWidgetQueryData(summaryQuery) ?? []; const t = useI18n(); const tDns = useI18n("widget.dnsHoleSummary"); const tWidgetCommon = useI18n("widget.common"); const successfulSummaries = summaries.filter(({ summary }) => summary !== null); const data = successfulSummaries.flatMap(({ summary }) => (summary ? [summary] : [])); const layoutProps = boxPropsByLayout(options.layout); const showSourceStatuses = successfulSummaries.length > 1 && width >= 240 && height >= 220; if (isInitialWidgetQueryPending(summaryQuery)) return ; return ( {data.length > 0 ? ( stats.map((item) => ( )) ) : ( {successfulSummaries.map(({ integration }) => ( ))} {tWidgetCommon("integrationDisconnected")} )} {showSourceStatuses && ( {successfulSummaries.map(({ integration, summary }) => ( {integration.name}: {tDns(`status.${summary?.status ?? "unknown"}` as never)} ))} )} ); } 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: "var(--mantine-color-red-light)", }, { 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: "var(--mantine-color-yellow-light)", }, { 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: "var(--mantine-color-cyan-light)", }, { 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: "var(--mantine-color-green-light)", }, ] 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(); const label = translateIfNecessary(t, item.label); const value = item.value(data, width <= 64 ? "sm" : "md"); const backgroundColor = usePiHoleColors ? `rgb(from ${item.color} r g b / calc(var(--opacity, 1) * 0.4))` : "rgb(from var(--mantine-color-primaryColor-filled) r g b / calc(var(--opacity, 1) * 0.12))"; return ( {value} {!hideLabel && ( {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, }, }; };