diff --git a/public/locales/en/modules/dns-hole-summary.json b/public/locales/en/modules/dns-hole-summary.json
index 207d37e1f..17c1149a6 100644
--- a/public/locales/en/modules/dns-hole-summary.json
+++ b/public/locales/en/modules/dns-hole-summary.json
@@ -6,6 +6,14 @@
"title": "Settings for DNS Hole summary",
"usePiHoleColors": {
"label": "Use colors from PiHole"
+ },
+ "layout": {
+ "label": "Layout",
+ "data": {
+ "grid": "2 by 2",
+ "row": "Horizontal",
+ "column": "Vertical"
+ }
}
}
},
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 8025ee4da..bcfcf4e42 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -109,57 +109,52 @@ function App(
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+ Switch: {
+ styles: {
+ input: { cursor: 'pointer' },
+ label: { cursor: 'pointer' },
+ },
+ },
+ },
+ primaryColor,
+ primaryShade,
+ colorScheme,
+ }}
+ withGlobalStyles
+ withNormalizeCSS
+ >
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 971c80786..2f3ff2ab5 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -4,7 +4,7 @@ import { appRouter } from './routers/app';
import { calendarRouter } from './routers/calendar';
import { configRouter } from './routers/config';
import { dashDotRouter } from './routers/dash-dot';
-import { dnsHoleRouter } from './routers/dns-hole';
+import { dnsHoleRouter } from './routers/dns-hole/router';
import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download';
import { iconRouter } from './routers/icon';
diff --git a/src/server/api/routers/dns-hole/router.ts b/src/server/api/routers/dns-hole/router.ts
index e21a3dc59..4d8bb8a76 100644
--- a/src/server/api/routers/dns-hole/router.ts
+++ b/src/server/api/routers/dns-hole/router.ts
@@ -6,7 +6,7 @@ import { PiHoleClient } from '~/tools/server/sdk/pihole/piHole';
import { ConfigAppType } from '~/types/app';
import { AdStatistics } from '~/widgets/dnshole/type';
-import { createTRPCRouter, publicProcedure } from '../trpc';
+import { createTRPCRouter, publicProcedure } from '../../trpc';
export const dnsHoleRouter = createTRPCRouter({
control: publicProcedure
diff --git a/src/tools/client/math.ts b/src/tools/client/math.ts
index ec2db7723..1b5e4de94 100644
--- a/src/tools/client/math.ts
+++ b/src/tools/client/math.ts
@@ -16,3 +16,7 @@ export const formatNumber = (n: number, decimalPlaces: number) => {
}
return n.toFixed(decimalPlaces);
};
+
+export const formatPercentage = (n: number, decimalPlaces: number) => {
+ return `${(n * 100).toFixed(decimalPlaces)}%`;
+};
diff --git a/src/widgets/dnshole/DnsHoleSummary.tsx b/src/widgets/dnshole/DnsHoleSummary.tsx
index 270f18dd5..9b25e5efd 100644
--- a/src/widgets/dnshole/DnsHoleSummary.tsx
+++ b/src/widgets/dnshole/DnsHoleSummary.tsx
@@ -1,20 +1,25 @@
-import { Card, Center, Container, Stack, Text } from '@mantine/core';
+import { Box, Card, Center, Container, Flex, Text } from '@mantine/core';
+import { useElementSize } from '@mantine/hooks';
import {
IconAd,
IconBarrierBlock,
IconPercentage,
IconSearch,
IconWorldWww,
+ TablerIconsProps,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
-import { api } from '~/utils/api';
+import { RouterOutputs, api } from '~/utils/api';
-import { formatNumber } from '../../tools/client/math';
+import { formatNumber, formatPercentage } from '../../tools/client/math';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
+const availableLayouts = ['grid', 'row', 'column'] as const;
+type AvailableLayout = (typeof availableLayouts)[number];
+
const definition = defineWidget({
id: 'dns-hole-summary',
icon: IconAd,
@@ -23,10 +28,15 @@ const definition = defineWidget({
type: 'switch',
defaultValue: true,
},
+ layout: {
+ type: 'select',
+ defaultValue: 'grid' as AvailableLayout,
+ data: availableLayouts.map((x) => ({ value: x })),
+ },
},
gridstack: {
minWidth: 2,
- minHeight: 2,
+ minHeight: 1,
maxWidth: 12,
maxHeight: 12,
},
@@ -40,7 +50,6 @@ interface DnsHoleSummaryWidgetProps {
}
function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
- const { t } = useTranslation('modules/dns-hole-summary');
const { isInitialLoading, data } = useDnsHoleSummeryQuery();
if (isInitialLoading || !data) {
@@ -48,139 +57,47 @@ function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
}
return (
-
- {
- 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
- >
-
-
-
-
- {formatNumber(data.adsBlockedToday, 0)}
-
- {t('card.metrics.queriesBlockedToday')}
-
-
-
-
-
- {
- 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
- >
-
-
-
- {(data.adsBlockedTodayPercentage * 100).toFixed(2)}%
-
-
-
- {
- 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
- >
-
-
-
-
- {formatNumber(data.dnsQueriesToday, 0)}
-
- {t('card.metrics.queriesToday')}
-
-
-
-
-
- {
- 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
- >
-
-
-
-
- {formatNumber(data.domainsBeingBlocked, 0)}
-
- {t('card.metrics.domainsOnAdlist')}
-
-
-
-
-
+
+ {stats.map((item) => (
+
+ ))}
);
}
+const stats = [
+ {
+ icon: IconBarrierBlock,
+ value: (x) => formatNumber(x.adsBlockedToday, 2),
+ label: 'card.metrics.queriesBlockedToday',
+ color: 'rgba(240, 82, 60, 0.4)',
+ },
+ {
+ icon: IconPercentage,
+ value: (x) => formatPercentage(x.adsBlockedTodayPercentage, 2),
+ color: 'rgba(255, 165, 20, 0.4)',
+ },
+ {
+ icon: IconSearch,
+ value: (x) => formatNumber(x.dnsQueriesToday, 2),
+ label: 'card.metrics.queriesToday',
+ color: 'rgba(0, 175, 218, 0.4)',
+ },
+ {
+ icon: IconWorldWww,
+ value: (x) => formatNumber(x.domainsBeingBlocked, 2),
+ label: 'card.metrics.domainsOnAdlist',
+ color: 'rgba(0, 176, 96, 0.4)',
+ },
+] satisfies StatItem[];
+
+type StatItem = {
+ icon: (props: TablerIconsProps) => JSX.Element;
+ value: (x: RouterOutputs['dnsHole']['summary']) => string;
+ label?: string;
+ color: string;
+};
+
export const useDnsHoleSummeryQuery = () => {
const { name: configName } = useConfigContext();
@@ -194,4 +111,71 @@ export const useDnsHoleSummeryQuery = () => {
);
};
+type StatCardProps = {
+ item: StatItem;
+ data: RouterOutputs['dnsHole']['summary'];
+ usePiHoleColors: boolean;
+};
+const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
+ const { t } = useTranslation('modules/dns-hole-summary');
+ const { ref, height, width } = useElementSize();
+ const isLong = width > height + 20;
+
+ return (
+
+
+
+
+
+
+ {item.value(data)}
+
+ {item.label && (
+
+ {t(item.label)}
+
+ )}
+
+
+
+
+ );
+};
+
+const constructContainerStyle = (flexLayout: (typeof availableLayouts)[number]) => {
+ if (flexLayout === 'grid') {
+ return {
+ display: 'grid',
+ gridTemplateColumns: '1fr 1fr',
+ gridTemplateRows: '1fr 1fr',
+ };
+ }
+
+ return {
+ display: 'flex',
+ flexDirection: flexLayout,
+ };
+};
+
export default definition;