feat: DnsHole feature parity with oldmarr (#1145)
* feat: DnsHole feature parity with oldmarr feat: advanced control management feat: disconnected state fix: summary widget sizing feat: summary text flash on update * feat: dnshole summary integrations disconnected error page * fix: classnaming * refactor: small rename, console to logger and unnecessary as conversion changes --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { BoxProps } from "@mantine/core";
|
||||
import { Box, Card, Flex, Text } from "@mantine/core";
|
||||
import { Avatar, AvatarGroup, Box, Card, Flex, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { formatNumber } from "@homarr/common";
|
||||
import { integrationDefs } from "@homarr/definitions";
|
||||
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||
import type { stringOrTranslation, TranslationFunction } from "@homarr/translation";
|
||||
import { translateIfNecessary } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { widgetKind } from ".";
|
||||
import type { WidgetComponentProps, WidgetProps } from "../../definition";
|
||||
import { NoIntegrationSelectedError } from "../../errors";
|
||||
|
||||
@@ -20,20 +24,65 @@ export default function DnsHoleSummaryWidget({
|
||||
options,
|
||||
integrationIds,
|
||||
serverData,
|
||||
}: WidgetComponentProps<"dnsHoleSummary">) {
|
||||
const integrationId = integrationIds.at(0);
|
||||
}: WidgetComponentProps<typeof widgetKind>) {
|
||||
const [summaries, setSummaries] = useState(serverData?.initialData ?? []);
|
||||
|
||||
if (!integrationId) {
|
||||
const t = useI18n();
|
||||
|
||||
clientApi.widget.dnsHole.subscribeToSummary.useSubscription(
|
||||
{
|
||||
widgetKind,
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
onData: (data) => {
|
||||
setSummaries((prevSummaries) =>
|
||||
prevSummaries.map((summary) => (summary.integration.id === data.integration.id ? data : summary)),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
summaries
|
||||
.filter(
|
||||
(
|
||||
pair,
|
||||
): pair is {
|
||||
integration: typeof pair.integration;
|
||||
timestamp: typeof pair.timestamp;
|
||||
summary: DnsHoleSummary;
|
||||
} => pair.summary !== null && Math.abs(dayjs(pair.timestamp).diff()) < 30000,
|
||||
)
|
||||
.flatMap(({ summary }) => summary),
|
||||
[summaries, serverData],
|
||||
);
|
||||
|
||||
if (integrationIds.length === 0) {
|
||||
throw new NoIntegrationSelectedError();
|
||||
}
|
||||
|
||||
const data = useMemo(() => (serverData?.initialData ?? []).flatMap((summary) => summary.summary), [serverData]);
|
||||
|
||||
return (
|
||||
<Box h="100%" {...boxPropsByLayout(options.layout)}>
|
||||
{stats.map((item, index) => (
|
||||
<StatCard key={index} item={item} usePiHoleColors={options.usePiHoleColors} data={data} />
|
||||
))}
|
||||
<Box h="100%" {...boxPropsByLayout(options.layout)} p="2cqmin">
|
||||
{data.length > 0 ? (
|
||||
stats.map((item) => (
|
||||
<StatCard key={item.color} item={item} usePiHoleColors={options.usePiHoleColors} data={data} t={t} />
|
||||
))
|
||||
) : (
|
||||
<Stack h="100%" w="100%" justify="center" align="center" gap="2.5cqmin" p="2.5cqmin">
|
||||
<AvatarGroup spacing="10cqmin">
|
||||
{summaries.map(({ integration }) => (
|
||||
<Tooltip key={integration.id} label={integration.name}>
|
||||
<Avatar h="35cqmin" w="35cqmin" src={integrationDefs[integration.kind].iconUrl} />
|
||||
</Tooltip>
|
||||
))}
|
||||
</AvatarGroup>
|
||||
<Text fz="10cqmin" ta="center">
|
||||
{t("widget.dnsHoleSummary.error.integrationsDisconnected")}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -86,26 +135,26 @@ const stats = [
|
||||
|
||||
interface StatItem {
|
||||
icon: TablerIcon;
|
||||
value: (x: RouterOutputs["widget"]["dnsHole"]["summary"][number]["summary"][], t: TranslationFunction) => string;
|
||||
value: (x: DnsHoleSummary[], t: TranslationFunction) => string;
|
||||
label: stringOrTranslation;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
item: StatItem;
|
||||
data: RouterOutputs["widget"]["dnsHole"]["summary"][number]["summary"][];
|
||||
data: DnsHoleSummary[];
|
||||
usePiHoleColors: boolean;
|
||||
t: TranslationFunction;
|
||||
}
|
||||
const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
||||
const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
|
||||
const { ref, height, width } = useElementSize();
|
||||
const isLong = width > height + 20;
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
className="summary-card"
|
||||
m="2.5cqmin"
|
||||
m="2cqmin"
|
||||
p="2.5cqmin"
|
||||
bg={usePiHoleColors ? item.color : "rgba(96, 96, 96, 0.1)"}
|
||||
style={{
|
||||
@@ -122,7 +171,7 @@ const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
||||
direction={isLong ? "row" : "column"}
|
||||
style={{ containerType: "size" }}
|
||||
>
|
||||
<item.icon className="summary-card-icon" size="50cqmin" style={{ margin: "2cqmin" }} />
|
||||
<item.icon className="summary-card-icon" size="40cqmin" style={{ margin: "2.5cqmin" }} />
|
||||
<Flex
|
||||
className="summary-card-texts"
|
||||
justify="center"
|
||||
@@ -134,11 +183,18 @@ const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
||||
h="100%"
|
||||
gap="1cqmin"
|
||||
>
|
||||
<Text className="summary-card-value" ta="center" size="25cqmin" fw="bold">
|
||||
<Text
|
||||
key={item.value(data, t)}
|
||||
className="summary-card-value text-flash"
|
||||
ta="center"
|
||||
size="20cqmin"
|
||||
fw="bold"
|
||||
style={{ "--glow-size": "2.5cqmin" }}
|
||||
>
|
||||
{item.value(data, t)}
|
||||
</Text>
|
||||
{item.label && (
|
||||
<Text className="summary-card-label" ta="center" size="17.5cqmin">
|
||||
<Text className="summary-card-label" ta="center" size="15cqmin">
|
||||
{translateIfNecessary(t, item.label)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("dnsHoleSummary", {
|
||||
export const widgetKind = "dnsHoleSummary";
|
||||
|
||||
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition(widgetKind, {
|
||||
icon: IconAd,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
usePiHoleColors: factory.switch({
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
|
||||
import { widgetKind } from ".";
|
||||
import type { WidgetProps } from "../../definition";
|
||||
|
||||
export default async function getServerDataAsync({ integrationIds }: WidgetProps<"dnsHoleSummary">) {
|
||||
export default async function getServerDataAsync({ integrationIds }: WidgetProps<typeof widgetKind>) {
|
||||
if (integrationIds.length === 0) {
|
||||
return {
|
||||
initialData: [],
|
||||
@@ -13,6 +14,7 @@ export default async function getServerDataAsync({ integrationIds }: WidgetProps
|
||||
|
||||
try {
|
||||
const currentDns = await api.widget.dnsHole.summary({
|
||||
widgetKind,
|
||||
integrationIds,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user