Add pihole integration (#860)

*  Add pihole integration

* Update src/widgets/adhole/AdHoleControls.tsx

Co-authored-by: Larvey <39219859+LarveyOfficial@users.noreply.github.com>

* Update src/tools/client/math.ts

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>

* Update src/widgets/dnshole/DnsHoleSummary.tsx

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>

---------

Co-authored-by: Larvey <39219859+LarveyOfficial@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2023-05-06 19:51:53 +02:00
committed by GitHub
parent 6ad799efe8
commit 92e8d79c5a
22 changed files with 1289 additions and 10 deletions

View File

@@ -0,0 +1,120 @@
import { Badge, Box, Button, Card, Group, Image, Stack, Text } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons';
import { useConfigContext } from '../../config/provider';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useDnsHoleControlMutation, useDnsHoleSummeryQuery } from './query';
import { PiholeApiSummaryType } from './type';
import { queryClient } from '../../tools/server/configurations/tanstack/queryClient.tool';
const definition = defineWidget({
id: 'dns-hole-controls',
icon: IconDeviceGamepad,
options: {},
gridstack: {
minWidth: 3,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
component: DnsHoleControlsWidgetTile,
});
export type IDnsHoleControlsWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface DnsHoleControlsWidgetProps {
widget: IDnsHoleControlsWidget;
}
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
const { isInitialLoading, data, refetch } = useDnsHoleSummeryQuery();
const { mutateAsync } = useDnsHoleControlMutation();
const { t } = useTranslation('modules/dns-hole-controls');
const { config } = useConfigContext();
if (isInitialLoading || !data) {
return <WidgetLoading />;
}
return (
<Stack>
<Group grow>
<Button
onClick={async () => {
await mutateAsync('enabled');
await queryClient.invalidateQueries({ queryKey: ['dns-hole-summary'] });
}}
leftIcon={<IconPlayerPlay size={20} />}
variant="light"
color="green"
>
{t('card.buttons.enableAll')}
</Button>
<Button
onClick={async () => {
await mutateAsync('disabled');
await queryClient.invalidateQueries({ queryKey: ['dns-hole-summary'] });
}}
leftIcon={<IconPlayerStop size={20} />}
variant="light"
color="red"
>
{t('card.buttons.disableAll')}
</Button>
</Group>
{data.status.map((status, index) => {
const app = config?.apps.find((x) => x.id === status.appId);
if (!app) {
return null;
}
return (
<Card withBorder key={index} p="xs">
<Group position="apart">
<Group>
<Box
sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2],
textAlign: 'center',
padding: 5,
borderRadius: theme.radius.md,
})}
>
<Image src={app.appearance.iconUrl} width={25} height={25} fit="contain" />
</Box>
<Text>{app.name}</Text>
</Group>
<StatusBadge status={status.status} />
</Group>
</Card>
);
})}
</Stack>
);
}
const StatusBadge = ({ status }: { status: PiholeApiSummaryType['status'] }) => {
const { t } = useTranslation('modules/dns-hole-controls');
if (status === 'enabled') {
return (
<Badge variant="dot" color="green">
{t('card.status.enabled')}
</Badge>
);
}
return (
<Badge variant="dot" color="red">
{t('card.status.disabled')}
</Badge>
);
};
export default definition;

View File

@@ -0,0 +1,181 @@
import { useTranslation } from 'next-i18next';
import { Card, Center, Container, Stack, Text } from '@mantine/core';
import { IconAd, IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from '@tabler/icons';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { formatNumber } from '../../tools/client/math';
import { useDnsHoleSummeryQuery } from './query';
const definition = defineWidget({
id: 'dns-hole-summary',
icon: IconAd,
options: {
usePiHoleColors: {
type: 'switch',
defaultValue: true,
},
},
gridstack: {
minWidth: 2,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
component: DnsHoleSummaryWidgetTile,
});
export type IDnsHoleSummaryWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface DnsHoleSummaryWidgetProps {
widget: IDnsHoleSummaryWidget;
}
function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
const { t } = useTranslation('modules/dns-hole-summary');
const { isInitialLoading, data } = useDnsHoleSummeryQuery();
if (isInitialLoading || !data) {
return <WidgetLoading />;
}
return (
<Container
display="grid"
h="100%"
style={{
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: '1fr 1fr',
marginLeft: -20,
marginRight: -20,
}}
>
<Card
m="xs"
sx={(theme) => {
if (!widget.properties.usePiHoleColors) {
return {};
}
if (theme.colorScheme === 'dark') {
return {
backgroundColor: 'rgba(240, 82, 60, 0.4)',
};
}
return {
backgroundColor: 'rgba(240, 82, 60, 0.2)',
};
}}
withBorder
>
<Center h="100%">
<Stack align="center" spacing="xs">
<IconBarrierBlock size={30} />
<div>
<Text align="center">{formatNumber(data.adsBlockedToday, 0)}</Text>
<Text align="center" lh={1.2} size="sm">
{t('card.metrics.queriesBlockedToday')}
</Text>
</div>
</Stack>
</Center>
</Card>
<Card
m="xs"
sx={(theme) => {
if (!widget.properties.usePiHoleColors) {
return {};
}
if (theme.colorScheme === 'dark') {
return {
backgroundColor: 'rgba(255, 165, 20, 0.4)',
};
}
return {
backgroundColor: 'rgba(255, 165, 20, 0.4)',
};
}}
withBorder
>
<Center h="100%">
<Stack align="center" spacing="xs">
<IconPercentage size={30} />
<div>
<Text align="center">{(data.adsBlockedTodayPercentage * 100).toFixed(2)}%</Text>
<Text align="center" lh={1.2} size="sm">
{t('card.metrics.queriesBlockedTodayPercentage')}
</Text>
</div>
</Stack>
</Center>
</Card>
<Card
m="xs"
sx={(theme) => {
if (!widget.properties.usePiHoleColors) {
return {};
}
if (theme.colorScheme === 'dark') {
return {
backgroundColor: 'rgba(0, 175, 218, 0.4)',
};
}
return {
backgroundColor: 'rgba(0, 175, 218, 0.4)',
};
}}
withBorder
>
<Center h="100%">
<Stack align="center" spacing="xs">
<IconSearch size={30} />
<div>
<Text align="center">{formatNumber(data.dnsQueriesToday, 0)}</Text>
<Text align="center" lh={1.2} size="sm">
{t('card.metrics.queriesToday')}
</Text>
</div>
</Stack>
</Center>
</Card>
<Card
m="xs"
sx={(theme) => {
if (!widget.properties.usePiHoleColors) {
return {};
}
if (theme.colorScheme === 'dark') {
return {
backgroundColor: 'rgba(0, 176, 96, 0.4)',
};
}
return {
backgroundColor: 'rgba(0, 176, 96, 0.4)',
};
}}
withBorder
>
<Center h="100%">
<Stack align="center" spacing="xs">
<IconWorldWww size={30} />
<div>
<Text align="center">{formatNumber(data.domainsBeingBlocked, 0)}</Text>
<Text align="center" lh={1.2} size="sm">
{t('card.metrics.domainsOnAdlist')}
</Text>
</div>
</Stack>
</Center>
</Card>
</Container>
);
}
export default definition;

View File

@@ -0,0 +1,23 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { AdStatistics, PiholeApiSummaryType } from './type';
export const useDnsHoleSummeryQuery = () =>
useQuery({
queryKey: ['dns-hole-summary'],
queryFn: async () => {
const response = await fetch('/api/modules/dns-hole/summary');
return (await response.json()) as AdStatistics;
},
refetchInterval: 3 * 60 * 1000,
});
export const useDnsHoleControlMutation = () =>
useMutation({
mutationKey: ['dns-hole-control'],
mutationFn: async (status: PiholeApiSummaryType['status']) => {
const response = await fetch(`/api/modules/dns-hole/control?status=${status}`, {
method: 'POST',
});
return response.json();
},
});

View File

@@ -0,0 +1,45 @@
export type AdStatistics = {
domainsBeingBlocked: number;
adsBlockedToday: number;
adsBlockedTodayPercentage: number;
dnsQueriesToday: number;
status: {
status: PiholeApiSummaryType['status'],
appId: string;
}[];
};
export type PiholeApiSummaryType = {
domains_being_blocked: number;
dns_queries_today: number;
ads_blocked_today: number;
ads_percentage_today: number;
unique_domains: number;
queries_forwarded: number;
queries_cached: number;
clients_ever_seen: number;
unique_clients: number;
dns_queries_all_types: number;
reply_UNKNOWN: number;
reply_NODATA: number;
reply_NXDOMAIN: number;
reply_CNAME: number;
reply_IP: number;
reply_DOMAIN: number;
reply_RRNAME: number;
reply_SERVFAIL: number;
reply_REFUSED: number;
reply_NOTIMP: number;
reply_OTHER: number;
reply_DNSSEC: number;
reply_NONE: number;
reply_BLOB: number;
dns_queries_all_replies: number;
privacy_level: number;
status: 'enabled' | 'disabled';
gravity_last_updated: {
file_exists: boolean;
absolute: number;
relative: { days: number; hours: number; minutes: number };
};
};

View File

@@ -11,6 +11,8 @@ import videoStream from './video/VideoStreamTile';
import weather from './weather/WeatherTile';
import mediaRequestsList from './media-requests/MediaRequestListTile';
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
import dnsHoleSummary from './dnshole/DnsHoleSummary';
import dnsHoleControls from './dnshole/DnsHoleControls';
export default {
calendar,
@@ -26,4 +28,6 @@ export default {
'media-server': mediaServer,
'media-requests-list': mediaRequestsList,
'media-requests-stats': mediaRequestsStats,
'dns-hole-summary': dnsHoleSummary,
'dns-hole-controls': dnsHoleControls,
};