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;