feat: add dns disable timer (#2029)
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"enableAll": "Enable all",
|
"enableAll": "Enable all",
|
||||||
"disableAll": "Disable all",
|
"disableAll": "Disable all",
|
||||||
|
"setTimer": "Set timer",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"changePosition": "Change position",
|
"changePosition": "Change position",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
|||||||
@@ -14,5 +14,12 @@
|
|||||||
"text": "There was a problem connecting to your DNS Hole(s). Please verify your configuration/integration(s)."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ export const dnsHoleRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
action: z.enum(['enable', 'disable']),
|
action: z.enum(['enable', 'disable']),
|
||||||
|
duration: z.number(),
|
||||||
configName: z.string(),
|
configName: z.string(),
|
||||||
appsToChange: z.optional(z.array(z.string())),
|
appsToChange: z.optional(z.array(z.string())),
|
||||||
})
|
})
|
||||||
@@ -32,12 +33,12 @@ export const dnsHoleRouter = createTRPCRouter({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
applicableApps.map(async (app) => {
|
applicableApps.map(async (app) => {
|
||||||
if (app.integration?.type === 'pihole') {
|
if (app.integration?.type === 'pihole') {
|
||||||
await processPiHole(app, input.action === 'enable');
|
await processPiHole(app, input.action === 'enable', input.duration);
|
||||||
|
|
||||||
return;
|
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(
|
const adGuard = new AdGuard(
|
||||||
app.url,
|
app.url,
|
||||||
findAppProperty(app, 'username'),
|
findAppProperty(app, 'username'),
|
||||||
@@ -106,13 +107,13 @@ const processAdGuard = async (app: ConfigAppType, enable: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adGuard.disable();
|
await adGuard.disable(duration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Consola.error((error as Error).message);
|
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'));
|
const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
|
||||||
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
@@ -125,7 +126,7 @@ const processPiHole = async (app: ConfigAppType, enable: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pihole.disable();
|
await pihole.disable(duration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Consola.error((error as Error).message);
|
Consola.error((error as Error).message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export class AdGuard {
|
|||||||
.reduce((sum, filter) => filter.rules_count + sum, 0);
|
.reduce((sum, filter) => filter.rules_count + sum, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disable() {
|
async disable(duration: number) {
|
||||||
await this.changeProtectionStatus(false);
|
await this.changeProtectionStatus(false, duration);
|
||||||
}
|
}
|
||||||
async enable() {
|
async enable() {
|
||||||
await this.changeProtectionStatus(true);
|
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
|
* 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 {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
|
* @returns {string} - The response from the AdGuard API
|
||||||
*/
|
*/
|
||||||
private async changeProtectionStatus(newStatus: boolean, duration = 0) {
|
private async changeProtectionStatus(newStatus: boolean, duration = 0) {
|
||||||
@@ -78,7 +78,7 @@ export class AdGuard {
|
|||||||
`${this.baseHostName}/control/protection`,
|
`${this.baseHostName}/control/protection`,
|
||||||
{
|
{
|
||||||
enabled: newStatus,
|
enabled: newStatus,
|
||||||
duration,
|
duration: duration * 1000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -37,16 +37,19 @@ export class PiHoleClient {
|
|||||||
return response.status === 'enabled';
|
return response.status === 'enabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
async disable() {
|
async disable(duration: number) {
|
||||||
const response = await this.sendStatusChangeRequest('disable');
|
const response = await this.sendStatusChangeRequest('disable', duration);
|
||||||
return response.status === 'disabled';
|
return response.status === 'disabled';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendStatusChangeRequest(
|
private async sendStatusChangeRequest(
|
||||||
action: 'enable' | 'disable'
|
action: 'enable' | 'disable',
|
||||||
|
duration = 0
|
||||||
): Promise<PiHoleApiStatusChangeResponse> {
|
): Promise<PiHoleApiStatusChangeResponse> {
|
||||||
const response = await fetch(
|
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) {
|
if (response.status !== 200) {
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Center,
|
Center,
|
||||||
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
|
Tooltip,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
|
import {
|
||||||
|
IconClockPause,
|
||||||
|
IconDeviceGamepad,
|
||||||
|
IconPlayerPlay,
|
||||||
|
IconPlayerStop,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useConfigContext } from '~/config/provider';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
@@ -23,6 +31,7 @@ import { defineWidget } from '../helper';
|
|||||||
import { WidgetLoading } from '../loading';
|
import { WidgetLoading } from '../loading';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { useDnsHoleSummeryQuery } from './DnsHoleSummary';
|
import { useDnsHoleSummeryQuery } from './DnsHoleSummary';
|
||||||
|
import { TimerModal } from './TimerModal';
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'dns-hole-controls',
|
id: 'dns-hole-controls',
|
||||||
@@ -69,9 +78,10 @@ const dnsLightStatus = (
|
|||||||
|
|
||||||
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
||||||
const { data: sessionData } = useSession();
|
const { data: sessionData } = useSession();
|
||||||
|
const [opened, { close, open }] = useDisclosure(false);
|
||||||
|
const [appId, setAppId] = useState('');
|
||||||
const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery();
|
const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery();
|
||||||
const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation();
|
const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation();
|
||||||
const { width, ref } = useElementSize();
|
|
||||||
const { t } = useTranslation(['common', 'modules/dns-hole-controls']);
|
const { t } = useTranslation(['common', 'modules/dns-hole-controls']);
|
||||||
|
|
||||||
const enableControls = sessionData?.user.isAdmin ?? false;
|
const enableControls = sessionData?.user.isAdmin ?? false;
|
||||||
@@ -124,10 +134,17 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
|||||||
return dnsList;
|
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(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
action,
|
action,
|
||||||
|
duration,
|
||||||
configName,
|
configName,
|
||||||
appsToChange,
|
appsToChange,
|
||||||
},
|
},
|
||||||
@@ -137,40 +154,68 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
setAppId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack justify="space-between" h={'100%'} spacing="0.25rem">
|
<Stack h="100%" spacing="0.25rem">
|
||||||
{enableControls && widget.properties.showToggleAllButtons && (
|
{enableControls && widget.properties.showToggleAllButtons && (
|
||||||
<SimpleGrid
|
<Flex gap="xs">
|
||||||
ref={ref}
|
<Tooltip label={t('enableAll')}>
|
||||||
cols={width > 275 ? 2 : 1}
|
<Button
|
||||||
verticalSpacing="0.25rem"
|
onClick={() => toggleDns('enable', getDnsStatus()?.disabled)}
|
||||||
spacing="0.25rem"
|
disabled={
|
||||||
>
|
getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus
|
||||||
<Button
|
}
|
||||||
onClick={() => toggleDns('enable', getDnsStatus()?.disabled)}
|
variant="light"
|
||||||
disabled={getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus}
|
color="green"
|
||||||
leftIcon={<IconPlayerPlay size={20} />}
|
fullWidth
|
||||||
variant="light"
|
h="2rem"
|
||||||
color="green"
|
>
|
||||||
h="2rem"
|
<IconPlayerPlay size={20} />
|
||||||
>
|
</Button>
|
||||||
{t('enableAll')}
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
<Button
|
<Tooltip label={t('setTimer')}>
|
||||||
onClick={() => toggleDns('disable', getDnsStatus()?.enabled)}
|
<Button
|
||||||
disabled={getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus}
|
onClick={open}
|
||||||
leftIcon={<IconPlayerStop size={20} />}
|
disabled={
|
||||||
variant="light"
|
getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus
|
||||||
color="red"
|
}
|
||||||
h="2rem"
|
variant="light"
|
||||||
>
|
color="yellow"
|
||||||
{t('disableAll')}
|
fullWidth
|
||||||
</Button>
|
h="2rem"
|
||||||
</SimpleGrid>
|
>
|
||||||
|
<IconClockPause size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label={t('disableAll')}>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleDns('disable', getDnsStatus()?.enabled)}
|
||||||
|
disabled={
|
||||||
|
getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
fullWidth
|
||||||
|
h="2rem"
|
||||||
|
>
|
||||||
|
<IconPlayerStop size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TimerModal
|
||||||
|
toggleDns={toggleDns}
|
||||||
|
getDnsStatus={getDnsStatus}
|
||||||
|
opened={opened}
|
||||||
|
close={close}
|
||||||
|
appId={appId}
|
||||||
|
/>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
spacing="0.25rem"
|
spacing="0.25rem"
|
||||||
display="flex"
|
display="flex"
|
||||||
@@ -203,36 +248,50 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Stack spacing="0rem">
|
<Stack spacing="0rem">
|
||||||
<Text>{app.name}</Text>
|
<Text>{app.name}</Text>
|
||||||
<UnstyledButton
|
<Flex direction="row" gap="md">
|
||||||
onClick={() =>
|
<UnstyledButton
|
||||||
toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id])
|
onClick={() =>
|
||||||
}
|
toggleDns(dnsHole.status === 'enabled' ? 'disable' : 'enable', [app.id])
|
||||||
disabled={fetchingDnsSummary || changingStatus}
|
}
|
||||||
style={{ pointerEvents: enableControls ? 'auto' : 'none' }}
|
disabled={fetchingDnsSummary || changingStatus}
|
||||||
>
|
style={{ pointerEvents: enableControls ? 'auto' : 'none' }}
|
||||||
<Badge
|
|
||||||
variant="dot"
|
|
||||||
color={dnsLightStatus(fetchingDnsSummary || changingStatus, dnsHole.status)}
|
|
||||||
styles={(theme) => ({
|
|
||||||
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)}
|
<Badge
|
||||||
</Badge>
|
variant="dot"
|
||||||
</UnstyledButton>
|
color={dnsLightStatus(fetchingDnsSummary || changingStatus, dnsHole.status)}
|
||||||
|
styles={(theme) => ({
|
||||||
|
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)}
|
||||||
|
</Badge>
|
||||||
|
</UnstyledButton>
|
||||||
|
<ActionIcon
|
||||||
|
size={20}
|
||||||
|
radius="xl"
|
||||||
|
top="2.67px"
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
setAppId(app.id);
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconClockPause size={20} color="red" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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 { useElementSize } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconAd,
|
IconAd,
|
||||||
@@ -112,7 +112,7 @@ export const useDnsHoleSummeryQuery = () => {
|
|||||||
configName: configName!,
|
configName: configName!,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
staleTime: 1000 * 60 * 2,
|
refetchInterval: 1000 * 60 * 2,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
124
src/widgets/dnshole/TimerModal.tsx
Normal file
124
src/widgets/dnshole/TimerModal.tsx
Normal file
@@ -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<NumberInputHandlers>();
|
||||||
|
const minutesHandlers = useRef<NumberInputHandlers>();
|
||||||
|
const { t } = useTranslation('modules/dns-hole-controls');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
withinPortal
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
size="sm"
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => {
|
||||||
|
close();
|
||||||
|
setHours(0);
|
||||||
|
setMinutes(0);
|
||||||
|
}}
|
||||||
|
title={t('modules/dns-hole-controls:durationModal.title')}
|
||||||
|
>
|
||||||
|
<Flex direction="column" align="center" justify="center">
|
||||||
|
<Stack align="flex-end">
|
||||||
|
<Group spacing={5}>
|
||||||
|
<Text>{t('modules/dns-hole-controls:durationModal.hours')}</Text>
|
||||||
|
<ActionIcon
|
||||||
|
size={35}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => hoursHandlers.current?.decrement()}
|
||||||
|
>
|
||||||
|
–
|
||||||
|
</ActionIcon>
|
||||||
|
<NumberInput
|
||||||
|
hideControls
|
||||||
|
value={hours}
|
||||||
|
onChange={(val) => setHours(Number(val))}
|
||||||
|
handlersRef={hoursHandlers}
|
||||||
|
max={999}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
styles={{ input: { width: rem(54), textAlign: 'center' } }}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
size={35}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => hoursHandlers.current?.increment()}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
<Group spacing={5}>
|
||||||
|
<Text>{t('modules/dns-hole-controls:durationModal.minutes')}</Text>
|
||||||
|
<ActionIcon
|
||||||
|
size={35}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => minutesHandlers.current?.decrement()}
|
||||||
|
>
|
||||||
|
–
|
||||||
|
</ActionIcon>
|
||||||
|
<NumberInput
|
||||||
|
hideControls
|
||||||
|
value={minutes}
|
||||||
|
onChange={(val) => setMinutes(Number(val))}
|
||||||
|
handlersRef={minutesHandlers}
|
||||||
|
max={59}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
styles={{ input: { width: rem(54), textAlign: 'center' } }}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
size={35}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => minutesHandlers.current?.increment()}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<Text ta="center" c="dimmed" my={5}>
|
||||||
|
{t('modules/dns-hole-controls:durationModal.unlimited')}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
leftIcon={<IconClockPause size={20} />}
|
||||||
|
h="2rem"
|
||||||
|
w="12rem"
|
||||||
|
onClick={() => {
|
||||||
|
toggleDns('disable', appId !== '' ? [appId] : getDnsStatus()?.enabled, hours, minutes);
|
||||||
|
setHours(0);
|
||||||
|
setMinutes(0);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('modules/dns-hole-controls:durationModal.set')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user