"use client"; import "../../widgets-common.css"; import { useState } from "react"; import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { useIntegrationsWithInteractAccess } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useIntegrationConnected } from "@homarr/common"; import { integrationDefs } from "@homarr/definitions"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import { MaskedOrNormalImage } from "@homarr/ui"; import type { widgetKind } from "."; import type { WidgetComponentProps } from "../../definition"; import TimerModal from "./TimerModal"; const dnsLightStatus = (enabled: boolean | undefined) => `var(--mantine-color-${typeof enabled === "undefined" ? "blue" : enabled ? "green" : "red"}-6`; export default function DnsHoleControlsWidget({ options, integrationIds, isEditMode, }: WidgetComponentProps) { const board = useRequiredBoard(); // DnsHole integrations with interaction permissions const integrationsWithInteractions = useIntegrationsWithInteractAccess() .map(({ id }) => id) .filter((id) => integrationIds.includes(id)); const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( { integrationIds, }, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, retry: false, }, ); const utils = clientApi.useUtils(); // Subscribe to summary updates clientApi.widget.dnsHole.subscribeToSummary.useSubscription( { integrationIds, }, { onData: (data) => { utils.widget.dnsHole.summary.setData( { integrationIds, }, (prevData) => { if (!prevData) return undefined; const newData = prevData.map((summary) => summary.integration.id === data.integration.id ? { integration: { ...summary.integration, updatedAt: new Date(), }, summary: data.summary, } : summary, ); return newData; }, ); }, }, ); // Mutations for dnsHole state, set to undefined on click, and change again on settle const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({ onSettled: (_, error, { integrationId }) => { utils.widget.dnsHole.summary.setData( { integrationIds, }, (prevData) => { if (!prevData) return []; return prevData.map((item) => item.integration.id === integrationId ? { ...item, summary: { ...item.summary, status: error ? "disabled" : "enabled", }, } : item, ); }, ); }, }); const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({ onSettled: (_, error, { integrationId }) => { utils.widget.dnsHole.summary.setData( { integrationIds, }, (prevData) => { if (!prevData) return []; return prevData.map((item) => item.integration.id === integrationId ? { ...item, summary: { ...item.summary, status: error ? "enabled" : "disabled", }, } : item, ); }, ); }, }); const toggleDns = (integrationId: string) => { const integrationStatus = summaries.find(({ integration }) => integration.id === integrationId); if (!integrationStatus?.summary.status) return; utils.widget.dnsHole.summary.setData( { integrationIds, }, (prevData) => { if (!prevData) return []; return prevData.map((item) => item.integration.id === integrationId ? { ...item, summary: { ...item.summary, status: undefined, }, } : item, ); }, ); if (integrationStatus.summary.status === "enabled") { disableDns({ integrationId, duration: 0 }); } else { enableDns({ integrationId }); } }; // make lists of enabled and disabled interactable integrations (with permissions, not disconnected and not processing) const integrationsSummaries = summaries.reduce( (acc, { summary, integration: { id } }) => integrationsWithInteractions.includes(id) && summary.status != null ? (acc[summary.status].push(id), acc) : acc, { enabled: [] as string[], disabled: [] as string[] }, ); const t = useI18n(); // Timer modal setup const [selectedIntegrationIds, setSelectedIntegrationIds] = useState([]); const [opened, { close, open }] = useDisclosure(false); const controlAllButtonsVisible = options.showToggleAllButtons && integrationsWithInteractions.length > 0; return ( {controlAllButtonsVisible && ( )} {summaries.map((summary) => ( ))} ); } interface ControlsCardProps { integrationsWithInteractions: string[]; toggleDns: (integrationId: string) => void; data: RouterOutputs["widget"]["dnsHole"]["summary"][number]; setSelectedIntegrationIds: (integrationId: string[]) => void; open: () => void; t: TranslationFunction; hasIconColor: boolean; } const ControlsCard: React.FC = ({ integrationsWithInteractions, toggleDns, data, setSelectedIntegrationIds, open, t, hasIconColor, }) => { const isConnected = useIntegrationConnected(data.integration.updatedAt, { timeout: 30000 }); const isEnabled = data.summary.status ? data.summary.status === "enabled" : undefined; const isInteractPermitted = integrationsWithInteractions.includes(data.integration.id); // Use all factors to infer the state of the action buttons const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected; const iconUrl = integrationDefs[data.integration.kind].iconUrl; return ( {data.integration.name} toggleDns(data.integration.id)} > ) } > {t( `widget.dnsHoleControls.controls.${ !isConnected ? "disconnected" : typeof isEnabled === "undefined" ? "processing" : isEnabled ? "enabled" : "disabled" }`, )} { setSelectedIntegrationIds([data.integration.id]); open(); }} > ); };