fix(widgets): resolve hooks violations, stale state, and missing edit-mode guard
- Move hooks above early returns in system-disks, immich-carousel, media-transcoding - Add enabled flag to immich-carousel query when albumId is absent - Replace disconnected local state in ping-indicator with query cache setData - Stabilize docker timestamp with useMemo to prevent useTimeAgo flicker - Wrap releases results in useMemo with proper type narrowing - Hide refresh indicator during edit mode
This commit is contained in:
6 files changed
+30
-33
No files matched your search
@@ -1,7 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { PingDot } from "./ping-dot";
|
||||
@@ -11,40 +9,33 @@ interface PingIndicatorProps {
|
||||
}
|
||||
|
||||
export const PingIndicator = ({ appId }: PingIndicatorProps) => {
|
||||
const { data: ping } = clientApi.widget.app.ping.useQuery(
|
||||
{
|
||||
id: appId,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
const { data: pingResult } = clientApi.widget.app.ping.useQuery(
|
||||
{ id: appId },
|
||||
{ refetchOnMount: false, refetchOnWindowFocus: false },
|
||||
);
|
||||
|
||||
const [pingResult, setPingResult] = useState<RouterOutputs["widget"]["app"]["ping"] | undefined>(ping);
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.app.updatedPing.useSubscription(
|
||||
{ id: appId },
|
||||
{
|
||||
onData(data) {
|
||||
setPingResult(data);
|
||||
utils.widget.app.ping.setData({ id: appId }, data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const current = pingResult ?? ping;
|
||||
if (!current) return null;
|
||||
if (!pingResult) return null;
|
||||
|
||||
const isError = "error" in current || current.statusCode >= 500;
|
||||
const isError = "error" in pingResult || pingResult.statusCode >= 500;
|
||||
|
||||
return (
|
||||
<PingDot
|
||||
icon={isError ? IconX : IconCheck}
|
||||
color={isError ? "red" : "green"}
|
||||
tooltip={
|
||||
"statusCode" in current
|
||||
? `${current.statusCode} - ${current.durationMs.toFixed(0)}ms`
|
||||
: current.error
|
||||
"statusCode" in pingResult
|
||||
? `${pingResult.statusCode} - ${pingResult.durationMs.toFixed(0)}ms`
|
||||
: pingResult.error
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -206,7 +206,8 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
const utils = clientApi.useUtils();
|
||||
const { data } = clientApi.docker.getContainers.useQuery();
|
||||
const containers = data?.containers ?? [];
|
||||
const relativeTime = useTimeAgo(data?.timestamp ?? new Date());
|
||||
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
|
||||
const relativeTime = useTimeAgo(timestamp);
|
||||
|
||||
clientApi.docker.subscribeContainers.useSubscription(undefined, {
|
||||
onData(data) {
|
||||
|
||||
@@ -17,15 +17,18 @@ export default function ImmichAlbumCarouselWidget({
|
||||
}: WidgetComponentProps<"immich-albumCarousel">) {
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||
|
||||
const { data: album } = clientApi.widget.immich.getAlbum.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
albumId: options.albumId ?? "",
|
||||
},
|
||||
{ enabled: Boolean(options.albumId) },
|
||||
);
|
||||
|
||||
if (!options.albumId) {
|
||||
return <NoAlbumSelected />;
|
||||
}
|
||||
|
||||
const { data: album } = clientApi.widget.immich.getAlbum.useQuery({
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
albumId: options.albumId,
|
||||
});
|
||||
|
||||
if (!album) return null;
|
||||
|
||||
if (album.assets.length === 0) {
|
||||
|
||||
@@ -51,12 +51,11 @@ export default function MediaTranscodingWidget({
|
||||
});
|
||||
|
||||
const [view, setView] = useState<View>(options.defaultView);
|
||||
const t = useI18n("widget.mediaTranscoding");
|
||||
|
||||
if (!transcodingData) return null;
|
||||
|
||||
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
|
||||
|
||||
const t = useI18n("widget.mediaTranscoding");
|
||||
const isTiny = width < 256;
|
||||
|
||||
return (
|
||||
|
||||
@@ -94,7 +94,10 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
),
|
||||
);
|
||||
|
||||
const results = queryResults.map((q) => q.data).filter(Boolean);
|
||||
const results = useMemo(
|
||||
() => queryResults.map((q) => q.data).filter((d): d is NonNullable<typeof d> => d != null),
|
||||
[queryResults],
|
||||
);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
const formattedResults = options.repositories
|
||||
|
||||
@@ -39,11 +39,6 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
const scheme = useMantineColorScheme();
|
||||
const t = useI18n();
|
||||
|
||||
const lastItem = data.at(-1);
|
||||
|
||||
if (!lastItem) return null;
|
||||
const { fileSystem, smart } = lastItem.healthInfo;
|
||||
|
||||
clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(queryInput, {
|
||||
onData(data) {
|
||||
utils.widget.healthMonitoring.getSystemHealthStatus.setData(queryInput, (oldData) =>
|
||||
@@ -52,6 +47,11 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
},
|
||||
});
|
||||
|
||||
const lastItem = data.at(-1);
|
||||
|
||||
if (!lastItem) return null;
|
||||
const { fileSystem, smart } = lastItem.healthInfo;
|
||||
|
||||
if (fileSystem.length === 0) {
|
||||
throw new NoIntegrationDataError();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user