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 (