mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
Merge pull request #1261 from Tagaishi/improve-dns-hole-stats-layout
This commit is contained in:
@@ -6,6 +6,14 @@
|
|||||||
"title": "Settings for DNS Hole summary",
|
"title": "Settings for DNS Hole summary",
|
||||||
"usePiHoleColors": {
|
"usePiHoleColors": {
|
||||||
"label": "Use colors from PiHole"
|
"label": "Use colors from PiHole"
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"label": "Layout",
|
||||||
|
"data": {
|
||||||
|
"grid": "2 by 2",
|
||||||
|
"row": "Horizontal",
|
||||||
|
"column": "Vertical"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,57 +109,52 @@ function App(
|
|||||||
<Head>
|
<Head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
<PersistQueryClientProvider
|
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||||
client={queryClient}
|
<ColorTheme.Provider value={colorTheme}>
|
||||||
persistOptions={{ persister: asyncStoragePersister }}
|
<MantineProvider
|
||||||
>
|
theme={{
|
||||||
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
...theme,
|
||||||
<ColorTheme.Provider value={colorTheme}>
|
components: {
|
||||||
<MantineProvider
|
Checkbox: {
|
||||||
theme={{
|
styles: {
|
||||||
...theme,
|
input: { cursor: 'pointer' },
|
||||||
components: {
|
label: { cursor: 'pointer' },
|
||||||
Checkbox: {
|
|
||||||
styles: {
|
|
||||||
input: { cursor: 'pointer' },
|
|
||||||
label: { cursor: 'pointer' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Switch: {
|
|
||||||
styles: {
|
|
||||||
input: { cursor: 'pointer' },
|
|
||||||
label: { cursor: 'pointer' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
primaryColor,
|
Switch: {
|
||||||
primaryShade,
|
styles: {
|
||||||
colorScheme,
|
input: { cursor: 'pointer' },
|
||||||
}}
|
label: { cursor: 'pointer' },
|
||||||
withGlobalStyles
|
},
|
||||||
withNormalizeCSS
|
},
|
||||||
>
|
},
|
||||||
<ConfigProvider {...props.pageProps}>
|
primaryColor,
|
||||||
<Notifications limit={4} position="bottom-left" />
|
primaryShade,
|
||||||
<ModalsProvider
|
colorScheme,
|
||||||
modals={{
|
}}
|
||||||
editApp: EditAppModal,
|
withGlobalStyles
|
||||||
selectElement: SelectElementModal,
|
withNormalizeCSS
|
||||||
integrationOptions: WidgetsEditModal,
|
>
|
||||||
integrationRemove: WidgetsRemoveModal,
|
<ConfigProvider {...props.pageProps}>
|
||||||
categoryEditModal: CategoryEditModal,
|
<Notifications limit={4} position="bottom-left" />
|
||||||
changeAppPositionModal: ChangeAppPositionModal,
|
<ModalsProvider
|
||||||
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
modals={{
|
||||||
}}
|
editApp: EditAppModal,
|
||||||
>
|
selectElement: SelectElementModal,
|
||||||
<Component {...pageProps} />
|
integrationOptions: WidgetsEditModal,
|
||||||
</ModalsProvider>
|
integrationRemove: WidgetsRemoveModal,
|
||||||
</ConfigProvider>
|
categoryEditModal: CategoryEditModal,
|
||||||
</MantineProvider>
|
changeAppPositionModal: ChangeAppPositionModal,
|
||||||
</ColorTheme.Provider>
|
changeIntegrationPositionModal: ChangeWidgetPositionModal,
|
||||||
</ColorSchemeProvider>
|
}}
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
>
|
||||||
</PersistQueryClientProvider>
|
<Component {...pageProps} />
|
||||||
|
</ModalsProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
</ColorTheme.Provider>
|
||||||
|
</ColorSchemeProvider>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { appRouter } from './routers/app';
|
|||||||
import { calendarRouter } from './routers/calendar';
|
import { calendarRouter } from './routers/calendar';
|
||||||
import { configRouter } from './routers/config';
|
import { configRouter } from './routers/config';
|
||||||
import { dashDotRouter } from './routers/dash-dot';
|
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 { dockerRouter } from './routers/docker/router';
|
||||||
import { downloadRouter } from './routers/download';
|
import { downloadRouter } from './routers/download';
|
||||||
import { iconRouter } from './routers/icon';
|
import { iconRouter } from './routers/icon';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { PiHoleClient } from '~/tools/server/sdk/pihole/piHole';
|
|||||||
import { ConfigAppType } from '~/types/app';
|
import { ConfigAppType } from '~/types/app';
|
||||||
import { AdStatistics } from '~/widgets/dnshole/type';
|
import { AdStatistics } from '~/widgets/dnshole/type';
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
import { createTRPCRouter, publicProcedure } from '../../trpc';
|
||||||
|
|
||||||
export const dnsHoleRouter = createTRPCRouter({
|
export const dnsHoleRouter = createTRPCRouter({
|
||||||
control: publicProcedure
|
control: publicProcedure
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ export const formatNumber = (n: number, decimalPlaces: number) => {
|
|||||||
}
|
}
|
||||||
return n.toFixed(decimalPlaces);
|
return n.toFixed(decimalPlaces);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatPercentage = (n: number, decimalPlaces: number) => {
|
||||||
|
return `${(n * 100).toFixed(decimalPlaces)}%`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
IconAd,
|
IconAd,
|
||||||
IconBarrierBlock,
|
IconBarrierBlock,
|
||||||
IconPercentage,
|
IconPercentage,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconWorldWww,
|
IconWorldWww,
|
||||||
|
TablerIconsProps,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useConfigContext } from '~/config/provider';
|
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 { defineWidget } from '../helper';
|
||||||
import { WidgetLoading } from '../loading';
|
import { WidgetLoading } from '../loading';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
|
|
||||||
|
const availableLayouts = ['grid', 'row', 'column'] as const;
|
||||||
|
type AvailableLayout = (typeof availableLayouts)[number];
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'dns-hole-summary',
|
id: 'dns-hole-summary',
|
||||||
icon: IconAd,
|
icon: IconAd,
|
||||||
@@ -23,10 +28,15 @@ const definition = defineWidget({
|
|||||||
type: 'switch',
|
type: 'switch',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'grid' as AvailableLayout,
|
||||||
|
data: availableLayouts.map((x) => ({ value: x })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
minWidth: 2,
|
minWidth: 2,
|
||||||
minHeight: 2,
|
minHeight: 1,
|
||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
maxHeight: 12,
|
maxHeight: 12,
|
||||||
},
|
},
|
||||||
@@ -40,7 +50,6 @@ interface DnsHoleSummaryWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
|
function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
|
||||||
const { t } = useTranslation('modules/dns-hole-summary');
|
|
||||||
const { isInitialLoading, data } = useDnsHoleSummeryQuery();
|
const { isInitialLoading, data } = useDnsHoleSummeryQuery();
|
||||||
|
|
||||||
if (isInitialLoading || !data) {
|
if (isInitialLoading || !data) {
|
||||||
@@ -48,139 +57,47 @@ function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container h="100%" p={0} style={constructContainerStyle(widget.properties.layout)}>
|
||||||
display="grid"
|
{stats.map((item) => (
|
||||||
h="100%"
|
<StatCard item={item} usePiHoleColors={widget.properties.usePiHoleColors} data={data} />
|
||||||
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} />
|
|
||||||
<Text align="center">{(data.adsBlockedTodayPercentage * 100).toFixed(2)}%</Text>
|
|
||||||
</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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
export const useDnsHoleSummeryQuery = () => {
|
||||||
const { name: configName } = useConfigContext();
|
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 (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
m="0.4rem"
|
||||||
|
p="0.2rem"
|
||||||
|
bg={usePiHoleColors ? item.color : 'rgba(96, 96, 96, 0.1)'}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
<Center h="100%" w="100%">
|
||||||
|
<Flex
|
||||||
|
h="100%"
|
||||||
|
w="100%"
|
||||||
|
align="center"
|
||||||
|
justify="space-evenly"
|
||||||
|
direction={isLong ? 'row' : 'column'}
|
||||||
|
>
|
||||||
|
<item.icon size={30} style={{ margin: '0 10' }} />
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
style={{
|
||||||
|
flex: isLong ? 1 : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text align="center" lh={1.2} size="md" weight="bold">
|
||||||
|
{item.value(data)}
|
||||||
|
</Text>
|
||||||
|
{item.label && (
|
||||||
|
<Text align="center" lh={1.2} size="0.75rem">
|
||||||
|
{t<string>(item.label)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Center>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
export default definition;
|
||||||
|
|||||||
Reference in New Issue
Block a user