Merge branch 'dev' into add-more-openapi

This commit is contained in:
Meier Lukas committed 2025-12-19 20:02:20 +01:00
commit f4cdf7aca6
323 files changed
+4555 -2926

No files matched your search

+24 -24
View File
@@ -31,13 +31,13 @@
"@homarr/auth": "workspace:^0.1.0",
"@homarr/boards": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/modals-collection": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
@@ -48,33 +48,33 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^8.3.9",
"@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@mantine/charts": "^8.3.10",
"@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10",
"@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "2.27.1",
"@tiptap/extension-highlight": "2.27.1",
"@tiptap/extension-image": "2.27.1",
"@tiptap/extension-link": "^2.27.1",
"@tiptap/extension-placeholder": "^2.27.1",
"@tiptap/extension-table": "2.27.1",
"@tiptap/extension-table-cell": "2.27.1",
"@tiptap/extension-table-header": "2.27.1",
"@tiptap/extension-table-row": "2.27.1",
"@tiptap/extension-task-item": "2.27.1",
"@tiptap/extension-task-list": "2.27.1",
"@tiptap/extension-text-align": "2.27.1",
"@tiptap/extension-text-style": "2.27.1",
"@tiptap/extension-underline": "2.27.1",
"@tiptap/react": "^2.27.1",
"@tiptap/starter-kit": "^2.27.1",
"@tiptap/extension-color": "3.13.0",
"@tiptap/extension-highlight": "3.13.0",
"@tiptap/extension-image": "3.13.0",
"@tiptap/extension-link": "^3.13.0",
"@tiptap/extension-placeholder": "^3.13.0",
"@tiptap/extension-table": "3.13.0",
"@tiptap/extension-table-cell": "3.13.0",
"@tiptap/extension-table-header": "3.13.0",
"@tiptap/extension-table-row": "3.13.0",
"@tiptap/extension-task-item": "3.13.0",
"@tiptap/extension-task-list": "3.13.0",
"@tiptap/extension-text-align": "3.13.0",
"@tiptap/extension-text-style": "3.13.0",
"@tiptap/extension-underline": "3.13.0",
"@tiptap/react": "^3.13.0",
"@tiptap/starter-kit": "^3.13.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"mantine-form-zod-resolver": "^1.3.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7",
"react": "19.2.1",
"react-dom": "19.2.1",
"next": "16.0.10",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"recharts": "^2.15.4",
"video.js": "^8.23.4",
@@ -85,7 +85,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"typescript": "^5.9.3"
}
}
+4 -2
View File
@@ -1,10 +1,12 @@
import { trpc } from "@homarr/api/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { db, inArray } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { Prefetch } from "../definition";
const logger = createLogger({ module: "appWidgetPrefetch" });
const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => {
const appIds = items.map((item) => item.options.appId);
const distinctAppIds = [...new Set(appIds)];
@@ -17,7 +19,7 @@ const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => {
queryClient.setQueryData(trpc.app.byId.queryKey({ id: app.id }), app);
}
logger.info(`Successfully prefetched ${dbApps.length} apps for app widget`);
logger.info("Successfully prefetched apps for app widget", { count: dbApps.length });
};
export default prefetchAllAsync;
+4 -2
View File
@@ -1,10 +1,12 @@
import { trpc } from "@homarr/api/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { db, inArray } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { Prefetch } from "../definition";
const logger = createLogger({ module: "bookmarksWidgetPrefetch" });
const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => {
const appIds = items.flatMap((item) => item.options.items);
const distinctAppIds = [...new Set(appIds)];
@@ -24,7 +26,7 @@ const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => {
);
}
logger.info(`Successfully prefetched ${dbApps.length} apps for bookmarks`);
logger.info("Successfully prefetched apps for bookmarks", { count: dbApps.length });
};
export default prefetchAllAsync;
+18 -3
View File
@@ -1,5 +1,6 @@
import { useState } from "react";
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CalendarEvent } from "@homarr/integrations/types";
@@ -17,6 +18,7 @@ interface CalendarDayProps {
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
const [opened, setOpened] = useState(false);
const { primaryColor } = useMantineTheme();
const { ref, height } = useElementSize();
const board = useRequiredBoard();
const mantineTheme = useMantineTheme();
const actualItemRadius = mantineTheme.radius[board.itemRadius];
@@ -25,6 +27,8 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
const shouldScaleDown = minAxisSize < 350;
const isSmall = rootHeight < 256;
const isTooSmallForIndicators = height < 30;
return (
<Popover
position="bottom"
@@ -47,6 +51,7 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
pt={isSmall ? 0 : 10}
pb={isSmall ? 0 : 10}
m={0}
ref={ref}
bd={`2px solid ${opened && !disabled ? primaryColor : "transparent"}`}
style={{
alignContent: "center",
@@ -62,7 +67,7 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
{date.getDate()}
</Text>
<NotificationIndicator events={events} isSmall={isSmall} />
{!isTooSmallForIndicators && <NotificationIndicator events={events} isSmall={isSmall} />}
</Container>
</Popover.Target>
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
@@ -82,9 +87,19 @@ const NotificationIndicator = ({ events, isSmall }: NotificationIndicatorProps)
const notificationEvents = [...new Set(events.map((event) => event.indicatorColor))].filter(String);
/* position bottom is lower when small to not be on top of number*/
return (
<Flex w="75%" pos={"absolute"} bottom={isSmall ? 4 : 10} left={"12.5%"} p={0} direction={"row"} justify={"center"}>
<Flex
w="75%"
align={"center"}
pos={"absolute"}
gap={3}
bottom={isSmall ? 4 : 10}
left={"12.5%"}
p={0}
direction={"row"}
justify={"center"}
>
{notificationEvents.map((notificationEvent) => {
return <Box key={notificationEvent} bg={notificationEvent} h={2} p={0} style={{ flex: 1, borderRadius: 5 }} />;
return <Box key={notificationEvent} bg={notificationEvent} h={4} w={4} p={0} style={{ borderRadius: 999 }} />;
})}
</Flex>
);
+9 -1
View File
@@ -124,12 +124,17 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
},
monthCell: {
textAlign: "center",
position: "relative",
},
day: {
borderRadius: actualItemRadius,
width: "100%",
height: "100%",
position: "relative",
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
},
month: {
height: "100%",
@@ -137,6 +142,9 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
weekday: {
padding: 0,
},
weekdaysRow: {
height: 22,
},
}}
renderDay={(tileDate) => {
const eventsForDate = normalizedEvents
+21 -2
View File
@@ -56,6 +56,7 @@ const createColumns = (
t: ReturnType<typeof useScopedI18n<"docker">>,
): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [
{
id: "name",
accessorKey: "name",
header: t("field.name.label"),
Cell({ renderedCellValue, row }) {
@@ -70,6 +71,7 @@ const createColumns = (
},
},
{
id: "state",
accessorKey: "state",
size: 100,
header: t("field.state.label"),
@@ -78,6 +80,13 @@ const createColumns = (
},
},
{
id: "cpuUsage",
sortingFn: (rowA, rowB) => {
const cpuUsageA = safeValue(rowA.original.cpuUsage);
const cpuUsageB = safeValue(rowB.original.cpuUsage);
return cpuUsageA - cpuUsageB;
},
accessorKey: "cpuUsage",
size: 80,
header: t("field.stats.cpu.label"),
@@ -92,6 +101,13 @@ const createColumns = (
},
},
{
id: "memoryUsage",
sortingFn: (rowA, rowB) => {
const memoryUsageA = safeValue(rowA.original.memoryUsage);
const memoryUsageB = safeValue(rowB.original.memoryUsage);
return memoryUsageA - memoryUsageB;
},
accessorKey: "memoryUsage",
size: 80,
header: t("field.stats.memory.label"),
@@ -106,9 +122,11 @@ const createColumns = (
},
},
{
id: "actions",
accessorKey: "actions",
size: 80,
header: t("action.title"),
enableSorting: false,
Cell({ row }) {
const utils = clientApi.useUtils();
// eslint-disable-next-line no-restricted-syntax
@@ -168,7 +186,7 @@ const createColumns = (
},
];
export default function DockerWidget({ width }: WidgetComponentProps<"dockerContainers">) {
export default function DockerWidget({ options, width, isEditMode }: WidgetComponentProps<"dockerContainers">) {
const t = useScopedI18n("docker");
const isTiny = width <= 256;
@@ -192,8 +210,8 @@ export default function DockerWidget({ width }: WidgetComponentProps<"dockerCont
enablePagination: false,
enableTopToolbar: false,
enableBottomToolbar: false,
enableSorting: false,
enableColumnActions: false,
enableSorting: options.enableRowSorting && !isEditMode,
enableStickyHeader: false,
enableColumnOrdering: false,
enableRowSelection: false,
@@ -203,6 +221,7 @@ export default function DockerWidget({ width }: WidgetComponentProps<"dockerCont
enableFilters: false,
enableHiding: false,
initialState: {
sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }],
density: "xs",
},
mantinePaperProps: {
+24 -1
View File
@@ -1,12 +1,35 @@
import { IconBrandDocker, IconServerOff } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
const columnsList = [
"name",
"state",
"cpuUsage",
"memoryUsage",
] as const satisfies (keyof RouterOutputs["docker"]["getContainers"]["containers"][number])[];
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
createOptions() {
return optionsBuilder.from(() => ({}));
return optionsBuilder.from((factory) => ({
enableRowSorting: factory.switch({
defaultValue: false,
}),
defaultSort: factory.select({
defaultValue: "name",
options: columnsList.map((value) => ({
value,
label: (t) => t(`widget.dockerContainers.option.defaultSort.option.${value}`),
})),
}),
descendingDefaultSort: factory.switch({
defaultValue: false,
}),
}));
},
errors: {
INTERNAL_SERVER_ERROR: {
+16 -7
View File
@@ -36,6 +36,7 @@ import {
IconX,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table";
import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
@@ -48,6 +49,8 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
dayjs.extend(relativeTime);
interface QuickFilter {
integrationKinds: string[];
statuses: ExtendedDownloadClientItem["state"][];
@@ -188,14 +191,14 @@ export default function DownloadClientsWidget({
)
//Add extrapolated data and actions if user is allowed interaction
.map((item): ExtendedDownloadClientItem => {
const received = Math.floor(item.size * item.progress);
const received = item.received ?? Math.floor(item.size * item.progress);
const integrationIds = [pair.integration.id];
return {
integration: pair.integration,
...item,
category: item.category !== undefined && item.category.length > 0 ? item.category : undefined,
received,
ratio: item.sent !== undefined ? item.sent / (received || 1) : undefined,
ratio: item.sent !== undefined ? item.sent / item.size : undefined,
//Only add if permission to use mutations
actions: integrationsWithInteractions.includes(pair.integration.id)
? {
@@ -470,13 +473,16 @@ export default function DownloadClientsWidget({
return (
<Group align="center" gap="xs" wrap="nowrap" w="100%">
<Text size="xs">
{new Intl.NumberFormat("en", { style: "percent", notation: "compact", unitDisplay: "narrow" }).format(
progress,
)}
{new Intl.NumberFormat("en", {
style: "percent",
notation: "compact",
unitDisplay: "narrow",
roundingMode: "floor",
}).format(progress)}
</Text>
<Progress
w="100%"
value={progress * 100}
value={Math.floor(progress * 100)}
color={row.original.state === "paused" ? "yellow" : progress === 1 ? "green" : "blue"}
radius="lg"
/>
@@ -714,7 +720,10 @@ const ItemInfoModal = ({ items, currentIndex, opened, onClose }: ItemInfoModalPr
/>
{item.type !== "miscellaneous" && <NormalizedLine itemKey="ratio" values={item.ratio} />}
<NormalizedLine itemKey="added" values={item.added === undefined ? "unknown" : dayjs(item.added).format()} />
<NormalizedLine itemKey="time" values={item.time !== 0 ? dayjs().add(item.time).format() : "∞"} />
<NormalizedLine
itemKey="time"
values={item.time !== 0 ? dayjs().add(item.time, "milliseconds").fromNow() : "∞"}
/>
<NormalizedLine itemKey="category" values={item.category} />
</Stack>
)}
+8 -4
View File
@@ -27,7 +27,7 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
className={classes.iframe}
src={embedUrl}
title="widget iframe"
allow={allowedPermissions.join(" ")}
allow={allowedPermissions}
scrolling={allowScrolling ? "yes" : "no"}
sandbox={sandboxFlags.join(" ")}
>
@@ -77,9 +77,13 @@ const UnsupportedProtocol = () => {
const getAllowedPermissions = (
permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">,
) => {
return objectEntries(permissions)
.filter(([_key, value]) => value)
.map(([key]) => permissionMapping[key]);
return (
objectEntries(permissions)
.filter(([_key, value]) => value)
// * means it applies to all origins
.map(([key]) => `${permissionMapping[key]} *`)
.join("; ")
);
};
const getSandboxFlags = (
+18 -16
View File
@@ -41,21 +41,21 @@ import {
IconX,
} from "@tabler/icons-react";
import { Color } from "@tiptap/extension-color";
import Highlight from "@tiptap/extension-highlight";
import Image from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder";
import Table from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import TextAlign from "@tiptap/extension-text-align";
import TextStyle from "@tiptap/extension-text-style";
import Underline from "@tiptap/extension-underline";
import { Highlight } from "@tiptap/extension-highlight";
import { Image } from "@tiptap/extension-image";
import { Placeholder } from "@tiptap/extension-placeholder";
import { Table } from "@tiptap/extension-table";
import { TableCell } from "@tiptap/extension-table-cell";
import { TableHeader } from "@tiptap/extension-table-header";
import { TableRow } from "@tiptap/extension-table-row";
import { TaskItem } from "@tiptap/extension-task-item";
import { TaskList } from "@tiptap/extension-task-list";
import { TextAlign } from "@tiptap/extension-text-align";
import { TextStyle } from "@tiptap/extension-text-style";
import type { Editor } from "@tiptap/react";
import { BubbleMenu, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useEditor } from "@tiptap/react";
import { BubbleMenu } from "@tiptap/react/menus";
import { StarterKit } from "@tiptap/starter-kit";
import type { Node } from "prosemirror-model";
import { clientApi } from "@homarr/api/client";
@@ -133,7 +133,8 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
};
},
}),
StarterKit,
// we use a custom link implementation from mantine
StarterKit.configure({ link: false }),
Table.configure({
resizable: true,
lastColumnResizable: false,
@@ -170,8 +171,9 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
TaskList.configure({ itemTypeName: "taskItem" }),
TextAlign.configure({ types: ["heading", "paragraph"] }),
TextStyle,
Underline,
],
shouldRerenderOnTransaction: true,
immediatelyRender: false,
content,
onUpdate: ({ editor }) => {
setContent(editor.getHTML());
+6 -6
View File
@@ -13,12 +13,12 @@ function round(value: number) {
return Math.round(value * 100) / 100;
}
function calculateChange(valueA: number, valueB: number) {
return valueA - valueB;
function calculateChange(currentPrice: number, previousClose: number) {
return currentPrice - previousClose;
}
function calculateChangePercentage(valueA: number, valueB: number) {
return 100 * ((valueA - valueB) / valueA);
function calculateChangePercentage(currentPrice: number, previousClose: number) {
return 100 * ((currentPrice - previousClose) / previousClose);
}
export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) {
@@ -26,9 +26,9 @@ export default function StockPriceWidget({ options, width, height }: WidgetCompo
const theme = useMantineTheme();
const [{ data }] = clientApi.widget.stockPrice.getPriceHistory.useSuspenseQuery(options);
const stockValuesChange = round(calculateChange(data.priceHistory.at(-1) ?? 0, data.priceHistory[0] ?? 0));
const stockValuesChange = round(calculateChange(data.priceHistory.at(-1) ?? 0, data.previousClose));
const stockValuesChangePercentage = round(
calculateChangePercentage(data.priceHistory.at(-1) ?? 0, data.priceHistory[0] ?? 0),
calculateChangePercentage(data.priceHistory.at(-1) ?? 0, data.previousClose),
);
const stockValuesMin = Math.min(...data.priceHistory);