diff --git a/apps/docs/docs/widgets/smart-home-entity-state/index.mdx b/apps/docs/docs/widgets/smart-home-entity-state/index.mdx index 28c83d3aa..9cd6d19a8 100644 --- a/apps/docs/docs/widgets/smart-home-entity-state/index.mdx +++ b/apps/docs/docs/widgets/smart-home-entity-state/index.mdx @@ -45,6 +45,7 @@ import temperature from "./img/temperature.png"; ### Interactions - Toggle entity state by clicking on the entity (only when clickable enabled). +- If Homarr cannot refresh an entity, the last known state stays visible but toggling is disabled until the connection recovers. diff --git a/packages/widgets/src/audio-stats/component.tsx b/packages/widgets/src/audio-stats/component.tsx index 5863b8ed4..12a4f25e7 100644 --- a/packages/widgets/src/audio-stats/component.tsx +++ b/packages/widgets/src/audio-stats/component.tsx @@ -1,15 +1,18 @@ "use client"; -import { Badge, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; +import { Badge, Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; +import { getQueryKey } from "@trpc/react-query"; import { clientApi } from "@homarr/api/client"; import { formatBytes, formatDuration } from "@homarr/common"; import type { AudiobookshelfDashboardData } from "@homarr/integrations/types"; +import { useI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; +import { IntegrationErrorIndicator } from "../common/integration-error-indicator"; +import { WidgetQueryErrorIndicator } from "../common/query-state-indicator"; import type { WidgetComponentProps } from "../definition"; -import { setWidgetRuntimeQueries } from "../definition"; -import { NoIntegrationDataError } from "../errors/no-data-integration"; +import { useWidgetRuntimeQueries } from "../runtime-hooks"; import { AudioStatsContent } from "./audio-stats-content"; export default function AudioStatsWidget({ @@ -18,8 +21,9 @@ export default function AudioStatsWidget({ width, height, displayMode = "compact", - widgetStateRef, + widgetRuntimeRef, }: WidgetComponentProps<"audioStats">) { + const t = useI18n(); const statsInput = { integrationId: integrationIds[0] ?? "" }; const { data: response, error: statsError } = clientApi.widget.audioStats.getStats.useQuery(statsInput); const streamsInput = { integrationIds: [statsInput.integrationId], showOnlyPlaying: true }; @@ -28,15 +32,14 @@ export default function AudioStatsWidget({ streamsInput, { enabled: streamsEnabled }, ); - setWidgetRuntimeQueries(widgetStateRef, [ - { path: ["widget", "audioStats", "getStats"], input: statsInput }, - ...(streamsEnabled ? [{ path: ["widget", "mediaServer", "getCurrentStreams"], input: streamsInput }] : []), + useWidgetRuntimeQueries(widgetRuntimeRef, [ + getQueryKey(clientApi.widget.audioStats.getStats, statsInput, "query"), + ...(streamsEnabled ? [getQueryKey(clientApi.widget.mediaServer.getCurrentStreams, streamsInput, "query")] : []), ]); if (statsError && response === undefined) throw statsError; if (streamsEnabled && streamsError && streamResults === undefined) throw streamsError; const currentStreams = streamResults ?? []; - if (streamsEnabled && currentStreams.some(({ error }) => Boolean(error))) throw new NoIntegrationDataError(); if (!response) return ; const summary = ( @@ -44,20 +47,38 @@ export default function AudioStatsWidget({ backend={response.kind} stats={response.data} options={{ ...options, compactMode: displayMode === "compact" && (options.compactMode || height < 160) }} + showAllStats={displayMode === "advanced"} width={width} height={height} /> ); - if (displayMode === "compact") return summary; + if (displayMode === "compact") { + return ( + + + + + {summary} + + ); + } const sessions = currentStreams.flatMap((result) => result.sessions.map((session) => ({ integrationId: result.integrationId, session })), ); const audiobookStats = response.kind === "audiobookshelf" ? (response.data as AudiobookshelfDashboardData) : null; const advancedGridColumns = width >= 760 ? 2 : 1; + const hasStreamErrors = currentStreams.some(({ error }) => Boolean(error)); return (
{summary}
+ {(statsError || streamsError || hasStreamErrors) && ( + + + + + + )} {response.kind === "navidrome" ? ( @@ -66,15 +87,22 @@ export default function AudioStatsWidget({ if (!currentlyPlaying) return null; return ( - + {currentlyPlaying.name} - {currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user.username} + {currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user?.username ?? "—"} - + {session.sessionName} diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index 94950135c..e3f776f66 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -50,7 +50,9 @@ export default function SmartHomeEntityStateWidget({ const apiUnit = entity?.attributes.unit_of_measurement; const unit = options.entityUnit || (typeof apiUnit === "string" ? apiUnit : ""); const attribute = unit.length > 0 ? ` ${unit}` : ""; - const isActionable = options.clickable && canInteract && entity !== undefined && !isEntityPending && !isPending; + const isActionable = + options.clickable && canInteract && entity !== undefined && !entityError && !isEntityPending && !isPending; + const queryErrorLabel = entityError ? t("widget.smartHome-entityState.error.loadFailed") : undefined; const handleClick = useCallback(() => { if (isEditMode) { @@ -108,7 +110,7 @@ export default function SmartHomeEntityStateWidget({ onClick={handleClick} disabled={!isActionable} aria-label={`${displayName}: ${state}${attribute}`} - aria-description={error?.message} + aria-description={queryErrorLabel ?? (error ? t("widget.smartHome-entityState.error.toggleFailed") : undefined)} w="100%" h="100%" styles={{ @@ -127,6 +129,11 @@ export default function SmartHomeEntityStateWidget({ {displayName} + {queryErrorLabel && ( + + {queryErrorLabel} + + )} {displayMode === "advanced" && entity && ( <>