diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7b1ad9bc7..fde418afc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,6 +31,9 @@ body: label: Version description: What version of Homarr are you running? options: + - 1.9.0 + - 1.8.0 + - 1.7.0 - 1.6.0 - 1.5.0 - 1.4.0 diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index cdf4ece79..cb7669807 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -56,9 +56,9 @@ "@mantine/tiptap": "^7.17.1", "@million/lint": "1.0.14", "@tabler/icons-react": "^3.31.0", - "@tanstack/react-query": "^5.67.1", - "@tanstack/react-query-devtools": "^5.67.1", - "@tanstack/react-query-next-experimental": "^5.67.1", + "@tanstack/react-query": "^5.67.2", + "@tanstack/react-query-devtools": "^5.67.2", + "@tanstack/react-query-next-experimental": "^5.67.2", "@trpc/client": "next", "@trpc/next": "next", "@trpc/react-query": "next", diff --git a/package.json b/package.json index 960ff7ad3..9d2fe6632 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "@semantic-release/release-notes-generator": "^14.0.3", "@turbo/gen": "^2.4.4", "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^3.0.7", - "@vitest/ui": "^3.0.7", + "@vitest/coverage-v8": "^3.0.8", + "@vitest/ui": "^3.0.8", "conventional-changelog-conventionalcommits": "^8.0.0", "cross-env": "^7.0.3", "jsdom": "^26.0.0", @@ -51,7 +51,7 @@ "turbo": "^2.4.4", "typescript": "^5.8.2", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.7" + "vitest": "^3.0.8" }, "packageManager": "pnpm@10.5.2", "engines": { diff --git a/packages/db/package.json b/packages/db/package.json index 3e763be54..80e57f99b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -52,7 +52,7 @@ "drizzle-kit": "^0.30.5", "drizzle-orm": "^0.40.0", "drizzle-zod": "^0.7.0", - "mysql2": "3.12.0" + "mysql2": "3.13.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts index 7970d7069..71e687927 100644 --- a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts +++ b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts @@ -1,3 +1,4 @@ +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { removeTrailingSlash } from "@homarr/common"; import type { IntegrationInput } from "../base/integration"; @@ -7,7 +8,7 @@ import { PiHoleIntegrationV6 } from "./v6/pi-hole-integration-v6"; export const createPiHoleIntegrationAsync = async (input: IntegrationInput) => { const baseUrl = removeTrailingSlash(input.url); const url = new URL(`${baseUrl}/api/info/version`); - const response = await fetch(url); + const response = await fetchWithTrustedCertificatesAsync(url); /** * In pi-hole 5 the api was at /admin/api.php, in pi-hole 6 it was moved to /api diff --git a/packages/log/package.json b/packages/log/package.json index 73bef5d32..3c4c4ce3e 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -24,7 +24,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/env": "workspace:^0.1.0", - "ioredis": "5.5.0", + "ioredis": "5.6.0", "superjson": "2.2.2", "winston": "3.17.0", "zod": "^3.24.2" diff --git a/packages/redis/package.json b/packages/redis/package.json index 260aa1449..e77630db6 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -26,7 +26,7 @@ "@homarr/db": "workspace:^", "@homarr/definitions": "workspace:^", "@homarr/log": "workspace:^", - "ioredis": "5.5.0", + "ioredis": "5.6.0", "superjson": "2.2.2" }, "devDependencies": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 73b99b8cd..458d98907 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -974,7 +974,7 @@ }, "option": { "borderColor": { - "label": "" + "label": "边界颜色" } }, "remove": { @@ -1822,10 +1822,10 @@ "available": "可用" }, "status": { - "pending": "", - "approved": "", - "declined": "", - "failed": "" + "pending": "待处理", + "approved": "已批准", + "declined": "已拒绝", + "failed": "失败" }, "toBeDetermined": "待定" }, diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index a7bcf29ff..e8f736f5d 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -974,7 +974,7 @@ }, "option": { "borderColor": { - "label": "" + "label": "צבע מסגרת" } }, "remove": { @@ -1822,10 +1822,10 @@ "available": "זמין" }, "status": { - "pending": "", - "approved": "", - "declined": "", - "failed": "" + "pending": "ממתין", + "approved": "אושר", + "declined": "נדחה", + "failed": "נכשל" }, "toBeDetermined": "ייקבע בהמשך" }, diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index f82d8348f..249b746b4 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -784,7 +784,7 @@ }, "tokenId": { "label": "Token Anahtar Kimliği", - "newLabel": "Yeni Anahtar Kimliği" + "newLabel": "Yeni Token Anahtar Kimliği" }, "realm": { "label": "Erişim Alanı", diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 6bbf39eab..fef5b5a67 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -974,7 +974,7 @@ }, "option": { "borderColor": { - "label": "" + "label": "邊框顏色" } }, "remove": { @@ -1822,10 +1822,10 @@ "available": "待定" }, "status": { - "pending": "", - "approved": "", - "declined": "", - "failed": "" + "pending": "待處理", + "approved": "已批准", + "declined": "已拒絕", + "failed": "失敗" }, "toBeDetermined": "多媒體請求狀態" }, diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index f7fd23b33..34a8dafb6 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -74,12 +74,11 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps< h="100%" w="100%" direction="column" - p="7.5cqmin" justify="center" align="center" > {options.showTitle && ( - + {app.name} )} diff --git a/packages/widgets/src/app/ping/ping-dot.tsx b/packages/widgets/src/app/ping/ping-dot.tsx index 81d3fa0ed..d728e8678 100644 --- a/packages/widgets/src/app/ping/ping-dot.tsx +++ b/packages/widgets/src/app/ping/ping-dot.tsx @@ -14,18 +14,18 @@ export const PingDot = ({ color, tooltip, ...props }: PingDotProps) => { const { pingIconsEnabled } = useSettings(); return ( - + {pingIconsEnabled ? ( - + ) : ( )} diff --git a/packages/widgets/src/bookmarks/bookmark.module.css b/packages/widgets/src/bookmarks/bookmark.module.css index b264ddd4c..c37b78242 100644 --- a/packages/widgets/src/bookmarks/bookmark.module.css +++ b/packages/widgets/src/bookmarks/bookmark.module.css @@ -2,6 +2,14 @@ background-color: var(--mantine-color-primaryColor-light-hover); } +[data-mantine-color-scheme="light"] .card-grid { + background-color: var(--mantine-color-gray-1); +} + +[data-mantine-color-scheme="dark"] .card-grid { + background-color: var(--mantine-color-dark-7); +} + .card:hover > div > div.bookmarkIcon { background-color: var(--mantine-color-iconColor-filled-hover); } diff --git a/packages/widgets/src/bookmarks/component.tsx b/packages/widgets/src/bookmarks/component.tsx index b1d15bd5c..a20093846 100644 --- a/packages/widgets/src/bookmarks/component.tsx +++ b/packages/widgets/src/bookmarks/component.tsx @@ -1,6 +1,7 @@ "use client"; import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core"; +import combineClasses from "clsx"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -42,9 +43,11 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg return ( - - {options.title} - + {options.title.length > 0 && ( + + {options.title} + + )} {options.layout === "grid" && ( { + const board = useRequiredBoard(); return ( - + {data.map((app, index) => (
- + {direction === "row" ? ( ) : ( @@ -134,12 +129,14 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, h // Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items const columns = Math.ceil(Math.sqrt(data.length * (width / height))); + const board = useRequiredBoard(); + return ( @@ -152,8 +149,19 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, h key={app.id} h="100%" > - - + + ))} @@ -166,15 +174,17 @@ const VerticalItem = ({ hideIcon, hideHostname, hasIconColor, + size = 30, }: { app: RouterOutputs["app"]["byIds"][number]; hideIcon: boolean; hideHostname: boolean; hasIconColor: boolean; + size?: number; }) => { return ( - - + + {app.name} {!hideIcon && ( @@ -184,16 +194,18 @@ const VerticalItem = ({ alt={app.name} className={classes.bookmarkIcon} style={{ - maxHeight: "100%", - maxWidth: "100%", + width: size, + height: size, overflow: "auto", flex: 1, scale: 0.8, + marginLeft: "auto", + marginRight: "auto", }} /> )} {!hideHostname && ( - + {app.href ? new URL(app.href).hostname : undefined} )} @@ -213,7 +225,7 @@ const HorizontalItem = ({ hasIconColor: boolean; }) => { return ( - + {!hideIcon && ( )} - + {app.name} {!hideHostname && ( - + {app.href ? new URL(app.href).hostname : undefined} )} diff --git a/packages/widgets/src/calendar/calender-day.tsx b/packages/widgets/src/calendar/calender-day.tsx index 698cfba3a..311d58049 100644 --- a/packages/widgets/src/calendar/calender-day.tsx +++ b/packages/widgets/src/calendar/calender-day.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; -import { Container, Popover, useMantineTheme } from "@mantine/core"; +import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core"; +import { useRequiredBoard } from "@homarr/boards/context"; import type { CalendarEvent } from "@homarr/integrations/types"; import { CalendarEventList } from "./calendar-event-list"; @@ -9,11 +10,19 @@ interface CalendarDayProps { date: Date; events: CalendarEvent[]; disabled: boolean; + rootWidth: number; + rootHeight: number; } -export const CalendarDay = ({ date, events, disabled }: CalendarDayProps) => { - const [opened, setOpend] = useState(false); +export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => { + const [opened, setOpened] = useState(false); const { primaryColor } = useMantineTheme(); + const board = useRequiredBoard(); + const mantineTheme = useMantineTheme(); + const actualItemRadius = mantineTheme.radius[board.itemRadius]; + + const minAxisSize = Math.min(rootWidth, rootHeight); + const shouldScaleDown = minAxisSize < 350; return ( { transitionProps={{ transition: "pop", }} - onChange={setOpend} + onChange={setOpened} opened={opened} disabled={disabled} > @@ -35,30 +44,23 @@ export const CalendarDay = ({ date, events, disabled }: CalendarDayProps) => { w="100%" p={0} m={0} - bd={`1cqmin solid ${opened && !disabled ? primaryColor : "transparent"}`} + bd={`3px solid ${opened && !disabled ? primaryColor : "transparent"}`} + pos={"relative"} style={{ alignContent: "center", - borderRadius: "3.5cqmin", + borderRadius: actualItemRadius, cursor: disabled ? "default" : "pointer", }} onClick={() => { if (disabled) return; - setOpend((prev) => !prev); + setOpened((prev) => !prev); }} > -
+ {date.getDate()} -
- +
+ {rootHeight >= 350 && } @@ -75,19 +77,10 @@ interface NotificationIndicatorProps { const NotificationIndicator = ({ events }: NotificationIndicatorProps) => { const notificationEvents = [...new Set(events.map((event) => event.links[0]?.notificationColor))].filter(String); return ( - + {notificationEvents.map((notificationEvent) => { - return ( - - ); + return ; })} - + ); }; diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index ec14f3e11..d05bbc7d8 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -2,11 +2,14 @@ import { useState } from "react"; import { useParams } from "next/navigation"; +import { useMantineTheme } from "@mantine/core"; import { Calendar } from "@mantine/dates"; +import { useElementSize } from "@mantine/hooks"; import dayjs from "dayjs"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import type { CalendarEvent } from "@homarr/integrations/types"; import { useSettings } from "@homarr/settings"; @@ -60,6 +63,10 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar const params = useParams(); const locale = params.locale as string; const { firstDayOfWeek } = useSettings(); + const board = useRequiredBoard(); + const mantineTheme = useMantineTheme(); + const actualItemRadius = mantineTheme.radius[board.itemRadius]; + const { ref, width, height } = useElementSize(); return ( Boolean(event.date)); return ( - + ); }} /> diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx index 95c5a2af2..32cf45549 100644 --- a/packages/widgets/src/clock/component.tsx +++ b/packages/widgets/src/clock/component.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import { Stack, Text } from "@mantine/core"; +import { Stack, Text, Title } from "@mantine/core"; import dayjs from "dayjs"; import advancedFormat from "dayjs/plugin/advancedFormat"; import timezones from "dayjs/plugin/timezone"; @@ -22,19 +22,19 @@ export default function ClockWidget({ options }: WidgetComponentProps<"clock">) const timezone = options.useCustomTimezone ? options.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone; const time = useCurrentTime(options); return ( - + {options.customTitleToggle && ( - + {options.customTitle} )} - + {options.customTimeFormat ? dayjs(time).tz(timezone).format(customTimeFormat) : dayjs(time).tz(timezone).format(timeFormat)} - </Text> + {options.showDate && ( - + {options.customDateFormat ? dayjs(time).tz(timezone).format(customDateFormat) : dayjs(time).tz(timezone).format(dateFormat)} diff --git a/packages/widgets/src/dns-hole/controls/component.module.css b/packages/widgets/src/dns-hole/controls/component.module.css new file mode 100644 index 000000000..1ab18a656 --- /dev/null +++ b/packages/widgets/src/dns-hole/controls/component.module.css @@ -0,0 +1,7 @@ +[data-mantine-color-scheme="light"] .card { + background-color: var(--mantine-color-gray-1); +} + +[data-mantine-color-scheme="dark"] .card { + background-color: var(--mantine-color-dark-7); +} diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index b7ad54202..7ee3576a4 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -6,6 +6,7 @@ import { useState } from "react"; import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react"; +import combineClasses from "clsx"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -19,6 +20,7 @@ import { MaskedOrNormalImage } from "@homarr/ui"; import type { widgetKind } from "."; import type { WidgetComponentProps } from "../../definition"; +import classes from "./component.module.css"; import TimerModal from "./TimerModal"; const dnsLightStatus = (enabled: boolean | undefined) => @@ -179,12 +181,12 @@ export default function DnsHoleControlsWidget({ className="dns-hole-controls-stack" h="100%" direction="column" - p="2.5cqmin" - gap="2.5cqmin" + p="sm" + gap="sm" style={{ pointerEvents: isEditMode ? "none" : undefined }} > {controlAllButtonsVisible && ( - + @@ -216,15 +215,12 @@ export default function DnsHoleControlsWidget({ variant="light" color="yellow" h="fit-content" - p="1.25cqmin" + p="xs" bd={0} - radius="2.5cqmin" + radius={board.itemRadius} flex={1} > - + @@ -236,15 +232,12 @@ export default function DnsHoleControlsWidget({ variant="light" color="red" h="fit-content" - p="1.25cqmin" + p="xs" bd={0} - radius="2.5cqmin" + radius={board.itemRadius} flex={1} > - + @@ -253,7 +246,7 @@ export default function DnsHoleControlsWidget({ @@ -306,34 +299,40 @@ const ControlsCard: React.FC = ({ const isInteractPermitted = integrationsWithInteractions.includes(data.integration.id); // Use all factors to infer the state of the action buttons const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected; + const board = useRequiredBoard(); const iconUrl = integrationDefs[data.integration.kind].iconUrl; return ( - + - - + + {data.integration.name} - + = ({ > ) } @@ -374,27 +371,25 @@ const ControlsCard: React.FC = ({ )} - { - setSelectedIntegrationIds([data.integration.id]); - open(); - }} - > - - + { + setSelectedIntegrationIds([data.integration.id]); + open(); + }} + > + + ); diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index 50d06de42..239d02295 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -2,11 +2,12 @@ import { useMemo } from "react"; import type { BoxProps } from "@mantine/core"; -import { Avatar, AvatarGroup, Box, Card, Flex, Stack, Text, Tooltip, TooltipFloating } from "@mantine/core"; +import { Avatar, AvatarGroup, Card, Flex, SimpleGrid, Stack, Text, Tooltip, TooltipFloating } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import { formatNumber } from "@homarr/common"; import { integrationDefs } from "@homarr/definitions"; import type { DnsHoleSummary } from "@homarr/integrations/types"; @@ -62,26 +63,26 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( - + {data.length > 0 ? ( stats.map((item) => ( )) ) : ( - - + + {summaries.map(({ integration }) => ( - + ))} - + {t("widget.dnsHoleSummary.error.integrationsDisconnected")} )} - + ); } @@ -152,30 +153,30 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => { const { ref, height, width } = useElementSize(); const isLong = width > height + 20; const tooltip = item.tooltip?.(data, t); + const board = useRequiredBoard(); return ( - + { style={{ flex: isLong ? 1 : undefined, }} + mt={"xs"} w="100%" - h="100%" - gap="1cqmin" + gap={0} > - + {item.value(data)} {item.label && ( - + {translateIfNecessary(t, item.label)} )} diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 39da51449..10c98ba8a 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -675,7 +675,7 @@ export default function DownloadClientsWidget({ void; availableStatuses: QuickFilter["statuses"]; - style?: MantineStyleProp; } -const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style }: ClientsControlProps) => { +const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: ClientsControlProps) => { const integrationsStatuses = clients.reduce( (acc, { status, integration: { id }, interact }) => status && interact ? (acc[status.paused ? "paused" : "active"].push(id), acc) : acc, @@ -799,33 +797,21 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0), "/s", ); - const chipStyle = { - "--chip-fz": "var(--button-fz)", - "--chip-size": "calc(var(--ratio-width) * 0.9)", - "--chip-icon-size": "calc(var(--chip-fz)*2/3)", - "--chip-padding": "var(--chip-fz)", - "--chip-checked-padding": "var(--chip-icon-size)", - "--chip-spacing": "var(--space-size)", - }; + const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation(); const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation(); const [opened, { open, close }] = useDisclosure(false); const t = useScopedI18n("widget.downloads"); return ( - + - + - - + + {t("items.integration.columnTitle")} setFilters({ ...filters, integrationKinds: names })} > {clients.map(({ integration }) => ( - + {integration.name} ))} @@ -845,7 +831,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })} > {availableStatuses.map((status) => ( - + {t(`states.${status}`)} ))} @@ -861,7 +847,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style {someInteract && ( { key={client.integration.id} src={getIconUrl(client.integration.kind)} style={{ filter: !isConnected ? "grayscale(100%)" : undefined }} - size="var(--image-size)" - p="calc(var(--space-size)*0.5)" + size={30} + p={5} bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`} /> ); diff --git a/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx b/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx new file mode 100644 index 000000000..0069c6d96 --- /dev/null +++ b/packages/widgets/src/health-monitoring/rings/cpu-ring.tsx @@ -0,0 +1,33 @@ +import { Box, Center, RingProgress, Text } from "@mantine/core"; +import { useElementSize } from "@mantine/hooks"; +import { IconCpu } from "@tabler/icons-react"; + +import { progressColor } from "../system-health"; + +export const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => { + const { width, ref } = useElementSize(); + const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 + + return ( + + + {`${cpuUtilization.toFixed(2)}%`} + + + } + sections={[ + { + value: Number(cpuUtilization.toFixed(2)), + color: progressColor(Number(cpuUtilization.toFixed(2))), + }, + ]} + /> + + ); +}; diff --git a/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx b/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx new file mode 100644 index 000000000..a70f26a90 --- /dev/null +++ b/packages/widgets/src/health-monitoring/rings/cpu-temp-ring.tsx @@ -0,0 +1,39 @@ +import { Box, Center, RingProgress, Text } from "@mantine/core"; +import { useElementSize } from "@mantine/hooks"; +import { IconCpu } from "@tabler/icons-react"; + +import { progressColor } from "../system-health"; + +export const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => { + const { width, ref } = useElementSize(); + const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 + + if (!cpuTemp) { + return null; + } + + return ( + + + + {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`} + + + + } + sections={[ + { + value: cpuTemp, + color: progressColor(cpuTemp), + }, + ]} + /> + + ); +}; diff --git a/packages/widgets/src/health-monitoring/rings/memory-ring.tsx b/packages/widgets/src/health-monitoring/rings/memory-ring.tsx new file mode 100644 index 000000000..2d8ce4d68 --- /dev/null +++ b/packages/widgets/src/health-monitoring/rings/memory-ring.tsx @@ -0,0 +1,54 @@ +import { Box, Center, RingProgress, Text } from "@mantine/core"; +import { useElementSize } from "@mantine/hooks"; +import { IconBrain } from "@tabler/icons-react"; + +import { progressColor } from "../system-health"; + +export const MemoryRing = ({ available, used }: { available: string; used: string }) => { + const { width, ref } = useElementSize(); + const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 + const memoryUsage = formatMemoryUsage(available, used); + + return ( + + + + {memoryUsage.memUsed.GB}GiB + + + + } + sections={[ + { + value: Number(memoryUsage.memUsed.percent), + color: progressColor(Number(memoryUsage.memUsed.percent)), + tooltip: `${memoryUsage.memUsed.percent}%`, + }, + ]} + /> + + ); +}; + +export const formatMemoryUsage = (memFree: string, memUsed: string) => { + const memFreeBytes = Number(memFree); + const memUsedBytes = Number(memUsed); + const totalMemory = memFreeBytes + memUsedBytes; + const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2); + const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2); + const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100); + const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100); + const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2); + + return { + memFree: { percent: memFreePercent, GB: memFreeGB }, + memUsed: { percent: memUsedPercent, GB: memUsedGB }, + memTotal: { GB: memTotalGB }, + }; +}; diff --git a/packages/widgets/src/health-monitoring/system-health.module.css b/packages/widgets/src/health-monitoring/system-health.module.css new file mode 100644 index 000000000..1ab18a656 --- /dev/null +++ b/packages/widgets/src/health-monitoring/system-health.module.css @@ -0,0 +1,7 @@ +[data-mantine-color-scheme="light"] .card { + background-color: var(--mantine-color-gray-1); +} + +[data-mantine-color-scheme="dark"] .card { + background-color: var(--mantine-color-dark-7); +} diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index d081132f9..82109f87a 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -1,10 +1,9 @@ "use client"; import { - Avatar, + ActionIcon, Box, Card, - Center, Divider, Flex, Group, @@ -12,12 +11,11 @@ import { List, Modal, Progress, - RingProgress, Stack, Text, Tooltip, } from "@mantine/core"; -import { useDisclosure, useElementSize } from "@mantine/hooks"; +import { useDisclosure } from "@mantine/hooks"; import { IconBrain, IconClock, @@ -29,14 +27,20 @@ import { IconTemperature, IconVersions, } from "@tabler/icons-react"; +import combineClasses from "clsx"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; +import { CpuRing } from "./rings/cpu-ring"; +import { CpuTempRing } from "./rings/cpu-temp-ring"; +import { formatMemoryUsage, MemoryRing } from "./rings/memory-ring"; +import classes from "./system-health.module.css"; dayjs.extend(duration); @@ -55,6 +59,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon ); const [opened, { open, close }] = useDisclosure(false); const utils = clientApi.useUtils(); + const board = useRequiredBoard(); clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( { integrationIds }, @@ -75,23 +80,21 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon ); return ( - + {healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => { const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart); const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed); return ( - + 0 ? "blue" : "gray"} position="top-end" - size="4cqmin" + size="md" label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined} disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0} > - - - + + + - - } - > + + }> {t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })} - } - > + }> {t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })} - } - > + }> {t("widget.healthMonitoring.popover.memoryAvailable", { memoryAvailable: memoryUsage.memFree.GB, percent: memoryUsage.memFree.percent, })} - } - > + }> {t("widget.healthMonitoring.popover.version", { version: healthInfo.version, })} - } - > + }> {formatUptime(healthInfo.uptime, t)} - } - > + }> {t("widget.healthMonitoring.popover.loadAverage")} - }> + }> {t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}% @@ -184,56 +175,53 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon {options.memory && } { - + {t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })} } - +
{options.fileSystem && disksData.map((disk) => { return ( - - - - + + + + {disk.deviceName} - - - + + + {options.fahrenheit ? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F` : `${disk.temperature}°C`} - - - + + + {disk.overallStatus ? disk.overallStatus : "N/A"} - + - + {t("widget.healthMonitoring.popover.used")} @@ -251,7 +239,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon value={100 - disk.percentage} color="default" > - + {t("widget.healthMonitoring.popover.available")} @@ -314,117 +302,3 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm }) .sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName)); }; - -const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => { - const { width, ref } = useElementSize(); - const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 - - return ( - - - {`${cpuUtilization.toFixed(2)}%`} - - - } - sections={[ - { - value: Number(cpuUtilization.toFixed(2)), - color: progressColor(Number(cpuUtilization.toFixed(2))), - }, - ]} - /> - - ); -}; - -const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => { - const { width, ref } = useElementSize(); - const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 - - if (!cpuTemp) { - return null; - } - - return ( - - - - {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`} - - - - } - sections={[ - { - value: cpuTemp, - color: progressColor(cpuTemp), - }, - ]} - /> - - ); -}; - -const MemoryRing = ({ available, used }: { available: string; used: string }) => { - const { width, ref } = useElementSize(); - const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196 - const memoryUsage = formatMemoryUsage(available, used); - - return ( - - - - {memoryUsage.memUsed.GB}GiB - - - - } - sections={[ - { - value: Number(memoryUsage.memUsed.percent), - color: progressColor(Number(memoryUsage.memUsed.percent)), - tooltip: `${memoryUsage.memUsed.percent}%`, - }, - ]} - /> - - ); -}; - -export const formatMemoryUsage = (memFree: string, memUsed: string) => { - const memFreeBytes = Number(memFree); - const memUsedBytes = Number(memUsed); - const totalMemory = memFreeBytes + memUsedBytes; - const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2); - const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2); - const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100); - const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100); - const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2); - - return { - memFree: { percent: memFreePercent, GB: memFreeGB }, - memUsed: { percent: memUsedPercent, GB: memUsedGB }, - memTotal: { GB: memTotalGB }, - }; -}; diff --git a/packages/widgets/src/indexer-manager/component.module.css b/packages/widgets/src/indexer-manager/component.module.css new file mode 100644 index 000000000..1ab18a656 --- /dev/null +++ b/packages/widgets/src/indexer-manager/component.module.css @@ -0,0 +1,7 @@ +[data-mantine-color-scheme="light"] .card { + background-color: var(--mantine-color-gray-1); +} + +[data-mantine-color-scheme="dark"] .card { + background-color: var(--mantine-color-dark-7); +} diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 1ba4fc273..12aed440a 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -2,11 +2,14 @@ import { Anchor, Button, Card, Container, Flex, Group, ScrollArea, Text } from "@mantine/core"; import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from "@tabler/icons-react"; +import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; +import classes from "./component.module.css"; export default function IndexerManagerWidget({ options, integrationIds }: WidgetComponentProps<"indexerManager">) { const t = useI18n(); @@ -22,6 +25,7 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget const utils = clientApi.useUtils(); const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation(); + const board = useRequiredBoard(); clientApi.widget.indexerManager.subscribeIndexersStatus.useSubscription( { integrationIds }, @@ -36,21 +40,28 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget }, ); - const iconStyle = { height: "7.5cqmin", width: "7.5cqmin" }; - return ( - - - {t("widget.indexerManager.title")} - - + + + + + {t("widget.indexerManager.title")} + + + {indexersData.map(({ integrationId, indexers }) => ( {indexers.map((indexer) => ( @@ -59,7 +70,7 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget href={indexer.url} target={options.openIndexerSiteInNewTab ? "_blank" : "_self"} > - + {indexer.name} @@ -67,13 +78,13 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget ) : ( )} @@ -85,11 +96,9 @@ export default function IndexerManagerWidget({ options, integrationIds }: Widget