diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx
index 220c2983a..6a2718505 100644
--- a/packages/widgets/src/anchor-note/component.tsx
+++ b/packages/widgets/src/anchor-note/component.tsx
@@ -14,6 +14,7 @@ import { useTimeAgo } from "@homarr/common";
import type { AnchorNotePermission } from "@homarr/integrations";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import "react-quill-new/dist/quill.snow.css";
@@ -264,7 +265,7 @@ const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteW
);
}, [canEdit, draftContent, draftTitle, hasChanges, integrationId, note, noteId, refetch, t, updateNoteAsync]);
- if (!note) return null;
+ if (!note) return ;
return (
diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx
index b2654b6eb..05a306db3 100644
--- a/packages/widgets/src/app/component.tsx
+++ b/packages/widgets/src/app/component.tsx
@@ -3,7 +3,7 @@
import type { PropsWithChildren } from "react";
import { Fragment, Suspense } from "react";
import { Flex, rem, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
-import { IconLoader } from "@tabler/icons-react";
+import { IconMinus } from "@tabler/icons-react";
import combineClasses from "clsx";
import { clientApi } from "@homarr/api/client";
@@ -13,6 +13,7 @@ import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
import { useI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import classes from "./app.module.css";
import { PingDot } from "./ping/ping-dot";
@@ -55,7 +56,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
[app, options.openInNewTab],
);
- if (!app) return null;
+ if (!app) return ;
const isTiny = height < 100 || width < 100;
const isColumnLayout = options.layout.startsWith("column");
@@ -131,7 +132,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
{options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? (
- }>
+ }>
) : null}
diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx
index 4ec836ca8..b8f88cdc8 100644
--- a/packages/widgets/src/app/ping/ping-indicator.tsx
+++ b/packages/widgets/src/app/ping/ping-indicator.tsx
@@ -1,6 +1,7 @@
-import { IconCheck, IconX } from "@tabler/icons-react";
+import { IconCheck, IconMinus, IconX } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
+import { useI18n } from "@homarr/translation/client";
import { PingDot } from "./ping-dot";
@@ -9,6 +10,7 @@ interface PingIndicatorProps {
}
export const PingIndicator = ({ appId }: PingIndicatorProps) => {
+ const t = useI18n();
const { data: pingResult } = clientApi.widget.app.ping.useQuery(
{ id: appId },
{ refetchOnMount: false, refetchOnWindowFocus: false },
@@ -24,7 +26,7 @@ export const PingIndicator = ({ appId }: PingIndicatorProps) => {
},
);
- if (!pingResult) return null;
+ if (!pingResult) return ;
const isError = "error" in pingResult || pingResult.statusCode >= 500;
diff --git a/packages/widgets/src/common/empty-state.tsx b/packages/widgets/src/common/empty-state.tsx
new file mode 100644
index 000000000..f5747b35c
--- /dev/null
+++ b/packages/widgets/src/common/empty-state.tsx
@@ -0,0 +1,15 @@
+import { Center, Text } from "@mantine/core";
+
+import { useI18n } from "@homarr/translation/client";
+
+export const WidgetEmptyState = () => {
+ const t = useI18n();
+
+ return (
+
+
+ {t("widget.common.error.noData")}
+
+
+ );
+};
diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
index 9f9d2a1bd..d8fa10614 100644
--- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
+++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx
@@ -5,6 +5,7 @@ import { clientApi } from "@homarr/api/client";
import type { Resource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import { formatUptime } from "../system-health";
import { ResourceAccordionItem } from "./resource-accordion-item";
@@ -56,7 +57,7 @@ export const ClusterHealthMonitoring = ({
},
);
- if (!healthData) return null;
+ if (!healthData) return ;
const activeNodes = healthData.nodes.reduce(running, 0);
const activeVMs = healthData.vms.reduce(running, 0);
diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx
index c26f63254..9c5260ff0 100644
--- a/packages/widgets/src/health-monitoring/system-health.tsx
+++ b/packages/widgets/src/health-monitoring/system-health.tsx
@@ -37,6 +37,7 @@ import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
+import { WidgetEmptyState } from "../common/empty-state";
import { CpuRing } from "./rings/cpu-ring";
import { CpuTempRing } from "./rings/cpu-temp-ring";
import { GpuRing } from "./rings/gpu-ring";
@@ -86,6 +87,8 @@ export const SystemHealthMonitoring = ({
const isTiny = width < 256;
+ if (healthData.length === 0) return ;
+
return (
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx
index cbaa44e07..9c157bfaa 100644
--- a/packages/widgets/src/immich/album-carousel/component.tsx
+++ b/packages/widgets/src/immich/album-carousel/component.tsx
@@ -8,6 +8,7 @@ import { IconAlertCircle, IconCalendar } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import classes from "./component.module.css";
@@ -29,7 +30,7 @@ export default function ImmichAlbumCarouselWidget({
return ;
}
- if (!album) return null;
+ if (!album) return ;
if (album.assets.length === 0) {
return ;
diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx
index 22346d376..919b0caf0 100644
--- a/packages/widgets/src/immich/server-stats/component.tsx
+++ b/packages/widgets/src/immich/server-stats/component.tsx
@@ -8,6 +8,7 @@ import { clientApi } from "@homarr/api/client";
import { humanFileSize } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import classes from "./component.module.css";
@@ -20,7 +21,7 @@ export default function ImmichServerStatsWidget({
integrationId: integrationIds[0] ?? "",
});
- if (!stats) return null;
+ if (!stats) return ;
return (
diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx
index 528f29fdd..4c60b1dcf 100644
--- a/packages/widgets/src/media-requests/list/component.tsx
+++ b/packages/widgets/src/media-requests/list/component.tsx
@@ -10,6 +10,7 @@ import type { MediaRequestStatus } from "@homarr/integrations/types";
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import { NoIntegrationDataError } from "../../errors/no-data-integration";
@@ -19,7 +20,7 @@ export default function MediaServerWidget({
options,
width,
}: WidgetComponentProps<"mediaRequests-requestList">) {
- const { data: mediaRequests = [], isFetched } = clientApi.widget.mediaRequests.getLatestRequests.useQuery(
+ const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery(
{
integrationIds,
},
@@ -58,7 +59,7 @@ export default function MediaServerWidget({
},
);
- if (!isFetched) return null;
+ if (!mediaRequests) return ;
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
return (
diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx
index 2119457c4..5a6f425b3 100644
--- a/packages/widgets/src/media-requests/stats/component.tsx
+++ b/packages/widgets/src/media-requests/stats/component.tsx
@@ -19,6 +19,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
import type { RequestStats } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import { NoIntegrationDataError } from "../../errors/no-data-integration";
import classes from "./component.module.css";
@@ -45,7 +46,7 @@ export default function MediaServerWidget({
const board = useRequiredBoard();
- if (!requestStats) return null;
+ if (!requestStats) return ;
if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError();
const data = [
diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx
index 5ab18cd83..494cfd968 100644
--- a/packages/widgets/src/media-transcoding/component.tsx
+++ b/packages/widgets/src/media-transcoding/component.tsx
@@ -9,6 +9,7 @@ import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { views } from ".";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { HealthCheckStatus } from "./health-check-status";
import { QueuePanel } from "./panels/queue.panel";
@@ -53,7 +54,7 @@ export default function MediaTranscodingWidget({
const [view, setView] = useState(options.defaultView);
const t = useI18n("widget.mediaTranscoding");
- if (!transcodingData) return null;
+ if (!transcodingData) return ;
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
const isTiny = width < 256;
diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx
index 630fb049f..c57cd5314 100644
--- a/packages/widgets/src/minecraft/server-status/component.tsx
+++ b/packages/widgets/src/minecraft/server-status/component.tsx
@@ -7,6 +7,7 @@ import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) {
@@ -22,7 +23,7 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
});
const tStatus = useScopedI18n("widget.minecraftServerStatus.status");
- if (!result) return null;
+ if (!result) return ;
const { data } = result;
const title = options.title.trim().length > 0 ? options.title : options.domain;
diff --git a/packages/widgets/src/speedtest-tracker/component.tsx b/packages/widgets/src/speedtest-tracker/component.tsx
index 4c9a0e22e..b55143084 100644
--- a/packages/widgets/src/speedtest-tracker/component.tsx
+++ b/packages/widgets/src/speedtest-tracker/component.tsx
@@ -8,6 +8,7 @@ import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
+import { WidgetEmptyState } from "../common/empty-state";
import { AveragesSection } from "./averages";
import { mergeStats } from "./helpers";
import { LatestResultSection } from "./latest-result";
@@ -86,6 +87,7 @@ export default function SpeedtestTrackerWidget({
{t("noSectionsEnabled")}
)}
+ {!noSectionsEnabled && !hasStatSection && !hasChart && }
);
}
diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx
index f5e64259b..918851ce8 100644
--- a/packages/widgets/src/stocks/component.tsx
+++ b/packages/widgets/src/stocks/component.tsx
@@ -7,6 +7,7 @@ import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
function round(value: number) {
@@ -26,7 +27,7 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
const theme = useMantineTheme();
const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options);
- if (!result) return null;
+ if (!result) return ;
const { data } = result;
const stockValuesChange = round(calculateChange(data.priceHistory.at(-1) ?? 0, data.previousClose));
diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx
index 99ee275b4..36b20e275 100644
--- a/packages/widgets/src/system-disks/component.tsx
+++ b/packages/widgets/src/system-disks/component.tsx
@@ -7,6 +7,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
import { humanFileSize } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { NoIntegrationDataError } from "../errors/no-data-integration";
@@ -49,7 +50,7 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
const lastItem = data.at(-1);
- if (!lastItem) return null;
+ if (!lastItem) return ;
const { fileSystem, smart } = lastItem.healthInfo;
if (fileSystem.length === 0) {
diff --git a/packages/widgets/src/system-resources/chart/common-chart.tsx b/packages/widgets/src/system-resources/chart/common-chart.tsx
index 2c25c8218..f2f9f1d87 100644
--- a/packages/widgets/src/system-resources/chart/common-chart.tsx
+++ b/packages/widgets/src/system-resources/chart/common-chart.tsx
@@ -2,7 +2,7 @@
import type { ReactNode } from "react";
import type { AreaChartSeries } from "@mantine/charts";
import { AreaChart, LineChart } from "@mantine/charts";
-import { Card, Center, Group, Loader, Stack, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core";
+import { Card, Center, Group, Stack, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core";
import { useElementSize, useHover, useMergedRef } from "@mantine/hooks";
import type { TooltipProps, YAxisProps } from "recharts";
@@ -85,8 +85,13 @@ export const CommonChart = ({
)}
{data.length <= 1 ? (
-
- 100 ? "md" : "xs"} color={"rgba(94, 94, 94, 1)"} />
+
+ {showIcon && 100 ? 20 : 14} stroke={1.5} />}
+ {showText && (
+ 100 ? "md" : "xs"} fw={"bold"} ta="center">
+ {title}
+
+ )}
) : (
diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx
index 60de018f4..cd25ff3c1 100644
--- a/packages/widgets/src/weather/component.tsx
+++ b/packages/widgets/src/weather/component.tsx
@@ -10,6 +10,7 @@ import { clientApi } from "@homarr/api/client";
import { metricToImperial } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
+import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { WeatherDescription, WeatherIcon } from "./icon";
@@ -29,7 +30,7 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr
onData: (data) => utils.widget.weather.atLocation.setData(input, data),
});
- if (!weather) return null;
+ if (!weather) return ;
return (