fix: harden widget review follow-ups

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:40 +02:00
1 parent 419c8434f9
commit c7431ef1f5
6 files changed
+37 -13

No files matched your search

@@ -3,6 +3,8 @@
import { ActionIcon, Anchor, Group, Indicator, Stack, Text } from "@mantine/core";
import { IconFileText, IconLink } from "@tabler/icons-react";
import { useScopedI18n } from "@homarr/translation/client";
import actionTargetClasses from "../common/action-target.module.css";
import { cleanFqdn, getResourceTimestamp, getStatusColor, parseStatus } from "./coolify-utils";
@@ -25,6 +27,7 @@ interface ResourceRowProps {
}
export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRowProps) {
const t = useScopedI18n("widget.coolify");
const status = parseStatus(item.status ?? "");
const statusColor = getStatusColor(status);
@@ -64,7 +67,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
href={cleanFqdn(item.fqdn)}
target="_blank"
rel="noopener noreferrer"
aria-label={`Open ${item.name}`}
aria-label={t("action.openResource", { name: item.name })}
size="xs"
variant="subtle"
c="dimmed"
@@ -79,7 +82,7 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
href={logsUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Open ${item.name} logs`}
aria-label={t("action.openLogs", { name: item.name })}
size="xs"
variant="subtle"
c="dimmed"
@@ -42,7 +42,7 @@ export function ServersSection({
</Group>
<ActionIcon
className={actionTargetClasses.root}
aria-label={`${showIp ? tCommon("action.hide") : tCommon("action.show")} IP`}
aria-label={t(showIp ? "action.hideIp" : "action.showIp")}
size="xs"
variant="subtle"
c="dimmed"
@@ -8,7 +8,7 @@ import objectSupport from "dayjs/plugin/objectSupport";
import relativeTime from "dayjs/plugin/relativeTime";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition";
import { WifiVariant } from "./variants/wifi-variant";
@@ -25,12 +25,17 @@ export default function NetworkControllerNetworkStatusWidget({
width,
height,
}: WidgetComponentProps<"networkControllerStatus">) {
const { data: summaries = [], isPending } = clientApi.widget.networkController.summary.useQuery({
const {
data: summaries,
error,
isPending,
} = clientApi.widget.networkController.summary.useQuery({
integrationIds,
});
const t = useI18n();
const t = useScopedI18n("widget.networkControllerStatus");
const tCommon = useScopedI18n("common");
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
const data = useMemo(() => (summaries ?? []).flatMap(({ summary }) => summary), [summaries]);
const isAdvanced = displayMode === "advanced";
const countWifiGuests = data.reduce((sum, summary) => sum + summary.wifi.guests, 0);
const countWifiUsers = data.reduce((sum, summary) => sum + summary.wifi.users, 0);
@@ -38,11 +43,13 @@ export default function NetworkControllerNetworkStatusWidget({
const countLanUsers = data.reduce((sum, summary) => sum + summary.lan.users, 0);
const useHorizontalStats = !isAdvanced && height < 150 && width >= 200;
if (error && summaries === undefined) throw error;
if (isPending || data.length === 0) {
return (
<Center h="100%" p="sm">
<Text c="dimmed" size="sm" ta="center">
{isPending ? t("common.action.loading") : t("widget.networkControllerStatus.error.integrationsDisconnected")}
{isPending ? tCommon("action.loading") : t("error.integrationsDisconnected")}
</Text>
</Center>
);
@@ -79,7 +79,11 @@ export default function PaperlessNgxWidget({
}: WidgetComponentProps<"paperlessNgx">) {
const t = useScopedI18n("widget.paperlessNgx");
const tCommon = useScopedI18n("common");
const { data: stats, isPending } = clientApi.widget.paperlessNgx.getStats.useQuery({
const {
data: stats,
error,
isPending,
} = clientApi.widget.paperlessNgx.getStats.useQuery({
integrationId: integrationIds[0] ?? "",
});
@@ -92,6 +96,7 @@ export default function PaperlessNgxWidget({
</Center>
);
}
if (error && stats === undefined) throw error;
if (!stats) return <WidgetEmptyState />;
const canShowInboxHero = options.showDocumentsInbox && options.showDocumentsTotal;
+3 -2
View File
@@ -3,7 +3,7 @@
import { Center, Flex, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { VpnIntegrationCard } from "./vpn-card";
@@ -21,6 +21,7 @@ export default function VpnWidget({
});
const t = useScopedI18n("widget.vpn");
const tCommon = useScopedI18n("common");
const locale = useCurrentLocale();
const dense = displayMode === "compact" && (width < 240 || height < 120);
if (isPending || integrations.length === 0) {
@@ -56,7 +57,7 @@ export default function VpnWidget({
<VpnIntegrationCard vpn={result.summary} integrationName={result.integration.name} variant="list" />
{!result.error && new Date(result.integration.updatedAt).getTime() > 0 && (
<Text size="xs" c="dimmed" ta="right">
{new Date(result.integration.updatedAt).toLocaleString()}
{new Date(result.integration.updatedAt).toLocaleString(locale)}
</Text>
)}
</Stack>
+10 -2
View File
@@ -102,8 +102,12 @@ function VpnStatusColumn({
compact: boolean;
dense: boolean;
}) {
const t = useScopedI18n("widget.vpn");
// Connected shows a shield with a check; anything else (stopped or unavailable) shows a shield with a cross.
const ShieldIcon = vpnStatus === RUNNING_STATUS ? IconShieldCheck : IconShieldX;
const statusLabel = t(
vpnStatus === RUNNING_STATUS ? "status.running" : vpnStatus ? "status.notRunning" : "status.unavailable",
);
return (
<Flex
@@ -114,7 +118,7 @@ function VpnStatusColumn({
align="center"
>
<ShieldIcon
aria-label={vpnStatus || "unavailable"}
aria-label={t("status.vpn", { status: statusLabel })}
stroke={2}
color={getStatusColor(vpnStatus)}
size={dense ? 32 : compact ? 44 : 52}
@@ -181,7 +185,11 @@ function VpnProviderDetails({ provider, protocol }: { provider: string; protocol
}
function DnsStatusBadge({ status }: { status: string }) {
const t = useScopedI18n("widget.vpn");
const StatusIcon = status === RUNNING_STATUS ? IconCircleCheckFilled : IconCircleXFilled;
const statusLabel = t(
status === RUNNING_STATUS ? "status.running" : status ? "status.notRunning" : "status.unavailable",
);
return (
<Badge
variant="outline"
@@ -191,7 +199,7 @@ function DnsStatusBadge({ status }: { status: string }) {
leftSection={<StatusIcon aria-hidden size={9} />}
>
DNS
<VisuallyHidden> {status || "unavailable"}</VisuallyHidden>
<VisuallyHidden> {statusLabel}</VisuallyHidden>
</Badge>
);
}