diff --git a/public/locales/en/common.json b/public/locales/en/common.json index bebc69de876..3f3b569225a 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -17,6 +17,7 @@ "disabled": "Disabled", "enableAll": "Enable all", "disableAll": "Disable all", + "setTimer": "Set timer", "version": "Version", "changePosition": "Change position", "remove": "Remove", diff --git a/public/locales/en/modules/dns-hole-controls.json b/public/locales/en/modules/dns-hole-controls.json index 2059f802aa2..562bf62afb9 100644 --- a/public/locales/en/modules/dns-hole-controls.json +++ b/public/locales/en/modules/dns-hole-controls.json @@ -14,5 +14,12 @@ "text": "There was a problem connecting to your DNS Hole(s). Please verify your configuration/integration(s)." } } + }, + "durationModal": { + "title": "Set disable duration time", + "hours": "Hours", + "minutes": "Minutes", + "unlimited": "leave empty for unlimited", + "set": "Set" } } \ No newline at end of file diff --git a/src/server/api/routers/dns-hole/router.ts b/src/server/api/routers/dns-hole/router.ts index 8f02ea981b5..d58039a5319 100644 --- a/src/server/api/routers/dns-hole/router.ts +++ b/src/server/api/routers/dns-hole/router.ts @@ -14,6 +14,7 @@ export const dnsHoleRouter = createTRPCRouter({ .input( z.object({ action: z.enum(['enable', 'disable']), + duration: z.number(), configName: z.string(), appsToChange: z.optional(z.array(z.string())), }) @@ -32,12 +33,12 @@ export const dnsHoleRouter = createTRPCRouter({ await Promise.all( applicableApps.map(async (app) => { if (app.integration?.type === 'pihole') { - await processPiHole(app, input.action === 'enable'); + await processPiHole(app, input.action === 'enable', input.duration); return; } - await processAdGuard(app, input.action === 'enable'); + await processAdGuard(app, input.action === 'enable', input.duration); }) ); }), @@ -89,7 +90,7 @@ export const dnsHoleRouter = createTRPCRouter({ }), }); -const processAdGuard = async (app: ConfigAppType, enable: boolean) => { +const processAdGuard = async (app: ConfigAppType, enable: boolean, duration: number = 0) => { const adGuard = new AdGuard( app.url, findAppProperty(app, 'username'), @@ -106,13 +107,13 @@ const processAdGuard = async (app: ConfigAppType, enable: boolean) => { } try { - await adGuard.disable(); + await adGuard.disable(duration); } catch (error) { Consola.error((error as Error).message); } }; -const processPiHole = async (app: ConfigAppType, enable: boolean) => { +const processPiHole = async (app: ConfigAppType, enable: boolean, duration: number = 0) => { const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey')); if (enable) { @@ -125,7 +126,7 @@ const processPiHole = async (app: ConfigAppType, enable: boolean) => { } try { - await pihole.disable(); + await pihole.disable(duration); } catch (error) { Consola.error((error as Error).message); } diff --git a/src/tools/server/sdk/adGuard/adGuard.ts b/src/tools/server/sdk/adGuard/adGuard.ts index 461b06f64aa..f1c631c29e1 100644 --- a/src/tools/server/sdk/adGuard/adGuard.ts +++ b/src/tools/server/sdk/adGuard/adGuard.ts @@ -59,8 +59,8 @@ export class AdGuard { .reduce((sum, filter) => filter.rules_count + sum, 0); } - async disable() { - await this.changeProtectionStatus(false); + async disable(duration: number) { + await this.changeProtectionStatus(false, duration); } async enable() { await this.changeProtectionStatus(true); @@ -69,7 +69,7 @@ export class AdGuard { /** * Make a post request to the AdGuard API to change the protection status based on the value of newStatus * @param {boolean} newStatus - The new status of the protection - * @param {number} duration - Duration of a pause, in milliseconds. Enabled should be false. + * @param {number} duration - Duration of a pause, in seconds. Enabled should be false. * @returns {string} - The response from the AdGuard API */ private async changeProtectionStatus(newStatus: boolean, duration = 0) { @@ -78,7 +78,7 @@ export class AdGuard { `${this.baseHostName}/control/protection`, { enabled: newStatus, - duration, + duration: duration * 1000, }, { headers: { diff --git a/src/tools/server/sdk/pihole/piHole.ts b/src/tools/server/sdk/pihole/piHole.ts index 76ed4517c2f..52c55867f81 100644 --- a/src/tools/server/sdk/pihole/piHole.ts +++ b/src/tools/server/sdk/pihole/piHole.ts @@ -37,16 +37,19 @@ export class PiHoleClient { return response.status === 'enabled'; } - async disable() { - const response = await this.sendStatusChangeRequest('disable'); + async disable(duration: number) { + const response = await this.sendStatusChangeRequest('disable', duration); return response.status === 'disabled'; } private async sendStatusChangeRequest( - action: 'enable' | 'disable' + action: 'enable' | 'disable', + duration = 0 ): Promise { const response = await fetch( - `${this.baseHostName}/admin/api.php?${action}&auth=${this.apiToken}` + duration !== 0 + ? `${this.baseHostName}/admin/api.php?${action}=${duration}&auth=${this.apiToken}` + : `${this.baseHostName}/admin/api.php?${action}&auth=${this.apiToken}` ); if (response.status !== 200) { diff --git a/src/widgets/dnshole/DnsHoleControls.tsx b/src/widgets/dnshole/DnsHoleControls.tsx index 872f891c375..8a543e6103b 100644 --- a/src/widgets/dnshole/DnsHoleControls.tsx +++ b/src/widgets/dnshole/DnsHoleControls.tsx @@ -1,21 +1,29 @@ import { + ActionIcon, Badge, Box, Button, Card, Center, + Flex, Group, Image, - SimpleGrid, Stack, Text, Title, + Tooltip, UnstyledButton, } from '@mantine/core'; -import { useElementSize } from '@mantine/hooks'; -import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; +import { useDisclosure } from '@mantine/hooks'; +import { + IconClockPause, + IconDeviceGamepad, + IconPlayerPlay, + IconPlayerStop, +} from '@tabler/icons-react'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; import { useConfigContext } from '~/config/provider'; import { api } from '~/utils/api'; @@ -23,6 +31,7 @@ import { defineWidget } from '../helper'; import { WidgetLoading } from '../loading'; import { IWidget } from '../widgets'; import { useDnsHoleSummeryQuery } from './DnsHoleSummary'; +import { TimerModal } from './TimerModal'; const definition = defineWidget({ id: 'dns-hole-controls', @@ -69,9 +78,10 @@ const dnsLightStatus = ( function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { const { data: sessionData } = useSession(); + const [opened, { close, open }] = useDisclosure(false); + const [appId, setAppId] = useState(''); const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery(); const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation(); - const { width, ref } = useElementSize(); const { t } = useTranslation(['common', 'modules/dns-hole-controls']); const enableControls = sessionData?.user.isAdmin ?? false; @@ -124,10 +134,17 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { return dnsList; }; - const toggleDns = async (action: 'enable' | 'disable', appsToChange?: string[]) => { + const toggleDns = async ( + action: 'enable' | 'disable', + appsToChange?: string[], + hours: number = 0, + minutes: number = 0 + ) => { + const duration = hours * 3600 + minutes * 60; await mutateAsync( { action, + duration, configName, appsToChange, }, @@ -137,40 +154,68 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { }, } ); + setAppId(''); }; return ( - + {enableControls && widget.properties.showToggleAllButtons && ( - 275 ? 2 : 1} - verticalSpacing="0.25rem" - spacing="0.25rem" - > - - - + + + + + + + + + + + + + )} + + {app.name} - - toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id]) - } - disabled={fetchingDnsSummary || changingStatus} - style={{ pointerEvents: enableControls ? 'auto' : 'none' }} - > - ({ - root: { - '&:hover': { - background: - theme.colorScheme === 'dark' - ? theme.colors.dark[4] - : theme.colors.gray[2], - }, - '&:active': { - background: - theme.colorScheme === 'dark' - ? theme.colors.dark[5] - : theme.colors.gray[3], + + + toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id]) + } + disabled={fetchingDnsSummary || changingStatus} + style={{ pointerEvents: enableControls ? 'auto' : 'none' }} + > + ({ + root: { + '&:hover': { + background: + theme.colorScheme === 'dark' + ? theme.colors.dark[4] + : theme.colors.gray[2], + }, + '&:active': { + background: + theme.colorScheme === 'dark' + ? theme.colors.dark[5] + : theme.colors.gray[3], + }, }, - }, - })} + })} + > + {t(dnsHole.status)} + + + { + setAppId(app.id); + open(); + }} > - {t(dnsHole.status)} - - + + + diff --git a/src/widgets/dnshole/DnsHoleSummary.tsx b/src/widgets/dnshole/DnsHoleSummary.tsx index b3451d23f4e..420b4ebe28a 100644 --- a/src/widgets/dnshole/DnsHoleSummary.tsx +++ b/src/widgets/dnshole/DnsHoleSummary.tsx @@ -1,4 +1,4 @@ -import { Box, Card, Center, Container, Flex, Text } from '@mantine/core'; +import { Card, Center, Container, Flex, Text } from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; import { IconAd, @@ -112,7 +112,7 @@ export const useDnsHoleSummeryQuery = () => { configName: configName!, }, { - staleTime: 1000 * 60 * 2, + refetchInterval: 1000 * 60 * 2, } ); }; diff --git a/src/widgets/dnshole/TimerModal.tsx b/src/widgets/dnshole/TimerModal.tsx new file mode 100644 index 00000000000..2ae41350227 --- /dev/null +++ b/src/widgets/dnshole/TimerModal.tsx @@ -0,0 +1,124 @@ +import { + ActionIcon, + Button, + Flex, + Group, + Modal, + NumberInput, + NumberInputHandlers, + Stack, + Text, + rem, +} from '@mantine/core'; +import { IconClockPause } from '@tabler/icons-react'; +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface TimerModalProps { + toggleDns: any; + getDnsStatus(): any; + opened: boolean; + close(): any; + appId: string; +} + +export function TimerModal({ toggleDns, getDnsStatus, opened, close, appId }: TimerModalProps) { + const [hours, setHours] = useState(0); + const [minutes, setMinutes] = useState(0); + const hoursHandlers = useRef(); + const minutesHandlers = useRef(); + const { t } = useTranslation('modules/dns-hole-controls'); + + return ( + { + close(); + setHours(0); + setMinutes(0); + }} + title={t('modules/dns-hole-controls:durationModal.title')} + > + + + + {t('modules/dns-hole-controls:durationModal.hours')} + hoursHandlers.current?.decrement()} + > + – + + setHours(Number(val))} + handlersRef={hoursHandlers} + max={999} + min={0} + step={1} + styles={{ input: { width: rem(54), textAlign: 'center' } }} + /> + hoursHandlers.current?.increment()} + > + + + + + + {t('modules/dns-hole-controls:durationModal.minutes')} + minutesHandlers.current?.decrement()} + > + – + + setMinutes(Number(val))} + handlersRef={minutesHandlers} + max={59} + min={0} + step={1} + styles={{ input: { width: rem(54), textAlign: 'center' } }} + /> + minutesHandlers.current?.increment()} + > + + + + + + + {t('modules/dns-hole-controls:durationModal.unlimited')} + + + + + ); +}