mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
🎨 Improve code structure of dns hole summary
This commit is contained in:
@@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
import { Card, Center, Container, Flex, Text } from '@mantine/core';
|
import { Box, Card, Center, Container, Flex, Text } from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconAd,
|
IconAd,
|
||||||
@@ -6,17 +6,21 @@ import {
|
|||||||
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 React from 'react';
|
import React from 'react';
|
||||||
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,
|
||||||
@@ -27,8 +31,8 @@ const definition = defineWidget({
|
|||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
defaultValue: 'grid',
|
defaultValue: 'grid' as AvailableLayout,
|
||||||
data: [{ value: 'grid' }, { value: 'row' }, { value: 'column' }],
|
data: availableLayouts.map((x) => ({ value: x })),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
@@ -47,60 +51,54 @@ 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();
|
||||||
const flexLayout = widget.properties.layout as 'row' | 'column';
|
|
||||||
|
|
||||||
if (isInitialLoading || !data) {
|
if (isInitialLoading || !data) {
|
||||||
return <WidgetLoading />;
|
return <WidgetLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container h="100%" p={0} style={constructContainerStyle(widget.properties.layout)}>
|
||||||
h="100%"
|
{stats.map((item) => (
|
||||||
p={0}
|
<StatCard item={item} usePiHoleColors={widget.properties.usePiHoleColors} data={data} />
|
||||||
style={{
|
))}
|
||||||
gridTemplateColumns: '1fr 1fr',
|
|
||||||
gridTemplateRows: '1fr 1fr',
|
|
||||||
display: flexLayout?.includes('grid') ? 'grid' : 'flex',
|
|
||||||
flexDirection: flexLayout,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StatCard
|
|
||||||
icon={<IconBarrierBlock />}
|
|
||||||
number={formatNumber(data.adsBlockedToday, 2)}
|
|
||||||
label={t('card.metrics.queriesBlockedToday') as string}
|
|
||||||
color={
|
|
||||||
widget.properties.usePiHoleColors ? 'rgba(240, 82, 60, 0.4)' : 'rgba(96, 96, 96, 0.1)'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={<IconPercentage />}
|
|
||||||
number={(data.adsBlockedTodayPercentage * 100).toFixed(2) + '%'}
|
|
||||||
color={
|
|
||||||
widget.properties.usePiHoleColors ? 'rgba(255, 165, 20, 0.4)' : 'rgba(96, 96, 96, 0.1)'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={<IconSearch />}
|
|
||||||
number={formatNumber(data.dnsQueriesToday, 2)}
|
|
||||||
label={t('card.metrics.queriesToday') as string}
|
|
||||||
color={
|
|
||||||
widget.properties.usePiHoleColors ? 'rgba(0, 175, 218, 0.4)' : 'rgba(96, 96, 96, 0.1)'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
icon={<IconWorldWww />}
|
|
||||||
number={formatNumber(data.domainsBeingBlocked, 2)}
|
|
||||||
label={t('card.metrics.domainsOnAdlist') as string}
|
|
||||||
color={
|
|
||||||
widget.properties.usePiHoleColors ? 'rgba(0, 176, 96, 0.4)' : 'rgba(96, 96, 96, 0.1)'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</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();
|
||||||
|
|
||||||
@@ -114,23 +112,23 @@ export const useDnsHoleSummeryQuery = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StatCardProps {
|
type StatCardProps = {
|
||||||
icon: JSX.Element;
|
item: StatItem;
|
||||||
number: string;
|
data: RouterOutputs['dnsHole']['summary'];
|
||||||
label?: string;
|
usePiHoleColors: boolean;
|
||||||
color?: string;
|
};
|
||||||
}
|
const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
||||||
|
const { t } = useTranslation('modules/dns-hole-summary');
|
||||||
const StatCard = ({ icon, number, label, color }: StatCardProps) => {
|
|
||||||
const { ref, height, width } = useElementSize();
|
const { ref, height, width } = useElementSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={ref}
|
ref={ref}
|
||||||
m="0.4rem"
|
m="0.4rem"
|
||||||
p="0.2rem"
|
p="0.2rem"
|
||||||
sx={{
|
bg={usePiHoleColors ? item.color : 'rgba(96, 96, 96, 0.1)'}
|
||||||
backgroundColor: color,
|
style={{
|
||||||
flex: '1',
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
@@ -142,31 +140,42 @@ const StatCard = ({ icon, number, label, color }: StatCardProps) => {
|
|||||||
justify="space-evenly"
|
justify="space-evenly"
|
||||||
direction={width > height + 20 ? 'row' : 'column'}
|
direction={width > height + 20 ? 'row' : 'column'}
|
||||||
>
|
>
|
||||||
{React.cloneElement(icon, {
|
<item.icon size={30} style={{ margin: '0 10' }} />
|
||||||
size: 30,
|
<Flex
|
||||||
style: { margin: '0 10' }
|
justify="center"
|
||||||
})}
|
direction="column"
|
||||||
<div
|
|
||||||
style={{
|
style={{
|
||||||
flex: '1',
|
flex: 1,
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text align="center" lh={1.2} size="md" weight="bold">
|
<Text align="center" lh={1.2} size="md" weight="bold">
|
||||||
{number}
|
{item.value(data)}
|
||||||
</Text>
|
</Text>
|
||||||
{label && (
|
{item.label && (
|
||||||
<Text align="center" lh={1.2} size="0.75rem">
|
<Text align="center" lh={1.2} size="0.75rem">
|
||||||
{label}
|
{t<string>(item.label)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Center>
|
</Center>
|
||||||
</Card>
|
</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