Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736c369164 | ||
|
|
f2d6563273 | ||
|
|
db3d58821e | ||
|
|
076526293d | ||
|
|
2443f1cd3f | ||
|
|
3864727502 | ||
|
|
f3608b26a0 | ||
|
|
73bed51df8 |
No files matched your search
@@ -33,6 +33,7 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.68.0
|
||||
- 1.67.0
|
||||
- 1.66.1
|
||||
- 1.66.0
|
||||
|
||||
@@ -163,65 +163,3 @@ jobs:
|
||||
run: cp .env.example .env
|
||||
- name: Build
|
||||
run: pnpm turbo build --affected
|
||||
|
||||
memory-regression:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
tags: homarr-memory-test
|
||||
network: host
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
- name: Start container and measure memory
|
||||
run: |
|
||||
docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test
|
||||
# Wait for container to be healthy (up to 60s)
|
||||
healthy=false
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:17575/api/health 2>/dev/null; then
|
||||
echo "Container healthy after $((i * 2))s"
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$healthy" != "true" ]; then
|
||||
echo "ERROR: Container never became healthy"
|
||||
docker logs homarr-mem-test --tail 50
|
||||
exit 1
|
||||
fi
|
||||
# Warm up: hit the dashboard page a few times
|
||||
for i in $(seq 1 5); do
|
||||
curl -sf http://localhost:17575/ > /dev/null 2>&1 || true
|
||||
sleep 2
|
||||
done
|
||||
# Measure RSS after warmup
|
||||
MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||')
|
||||
echo "Raw memory: $MEM_KB"
|
||||
# Convert to numeric MB (handles GiB and MiB suffixes)
|
||||
MEM_NUM=$(echo "$MEM_KB" | awk '{
|
||||
if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 }
|
||||
else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 }
|
||||
else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 }
|
||||
else { print $1 / 1048576 }
|
||||
}')
|
||||
echo "Memory usage: ${MEM_NUM} MB"
|
||||
MAX_MB=700
|
||||
if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then
|
||||
echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold"
|
||||
docker logs homarr-mem-test --tail 50
|
||||
exit 1
|
||||
fi
|
||||
echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB"
|
||||
@@ -1,10 +1,3 @@
|
||||
## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
|
||||
* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
|
||||
|
||||
## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26)
|
||||
|
||||
### Features
|
||||
|
||||
+14
@@ -20,6 +20,17 @@ ARG DISABLE_REDIS_LOGS='true'
|
||||
|
||||
RUN corepack enable pnpm && pnpm build
|
||||
|
||||
# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is
|
||||
# where production Node stack frames point. Copy the .map files into the standalone tree (preserving
|
||||
# directory structure) so the existing standalone COPY ships them, keeping traces readable.
|
||||
# See https://github.com/homarr-labs/homarr/issues/5891
|
||||
RUN cd apps/nextjs/.next/server && \
|
||||
find . -name '*.map' | while IFS= read -r map; do \
|
||||
dest="../standalone/apps/nextjs/.next/server/$map"; \
|
||||
mkdir -p "$(dirname "$dest")"; \
|
||||
cp "$map" "$dest"; \
|
||||
done
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@@ -64,6 +75,9 @@ ENV DB_DRIVER='better-sqlite3'
|
||||
ENV AUTH_PROVIDERS='credentials'
|
||||
ENV REDIS_IS_EXTERNAL='false'
|
||||
ENV NODE_ENV='production'
|
||||
# Tell Node to symbolicate stack traces using the shipped .map files so production logs
|
||||
# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891
|
||||
ENV NODE_OPTIONS='--enable-source-maps'
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD ["sh", "run.sh"]
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A Helm chart to deploy homarr for Kubernetes
|
||||
|
||||
@@ -383,7 +383,6 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
|
||||
| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. |
|
||||
| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). |
|
||||
| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql |
|
||||
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
|
||||
| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server |
|
||||
| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups |
|
||||
| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups |
|
||||
@@ -396,6 +395,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
|
||||
| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) |
|
||||
| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field |
|
||||
| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. |
|
||||
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
|
||||
| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login |
|
||||
| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) |
|
||||
| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim |
|
||||
|
||||
@@ -63,7 +63,9 @@ import withoutIntegrations from './img/without-integrations.png';
|
||||
|
||||
### Interactions
|
||||
|
||||
- View calendar events by clicking on the marked dates.
|
||||
- Hover over a marked date to preview its events in a popover.
|
||||
- Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away).
|
||||
- Click the date again or click outside to close the pinned popover.
|
||||
|
||||
### Adding the widget
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
// Ship browser source maps so production client stack traces are readable, see https://github.com/homarr-labs/homarr/issues/5891
|
||||
productionBrowserSourceMaps: true,
|
||||
// react compiler breaks mantine-react-table, so disabled for now
|
||||
//reactCompiler: true,
|
||||
/** We already do typechecking as separate tasks in CI */
|
||||
@@ -41,6 +43,8 @@ const nextConfig: NextConfig = {
|
||||
*/
|
||||
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom", "better-sqlite3"],
|
||||
experimental: {
|
||||
// Ship server source maps so production Node stack traces in logs reference original source, see https://github.com/homarr-labs/homarr/issues/5891
|
||||
serverSourceMaps: true,
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||
turbopackFileSystemCacheForDev: true,
|
||||
preloadEntriesOnStart: false,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.68.1",
|
||||
"version": "1.68.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { hashKey } from "@tanstack/query-core";
|
||||
|
||||
export function objectKeys<O extends object>(obj: O): (keyof O)[] {
|
||||
return Object.keys(obj) as (keyof O)[];
|
||||
}
|
||||
@@ -11,5 +9,18 @@ type Entries<T> = {
|
||||
export const objectEntries = <T extends object>(obj: T) => Object.entries(obj) as Entries<T>;
|
||||
|
||||
export const hashObjectBase64 = (obj: object) => {
|
||||
return Buffer.from(hashKey([obj])).toString("base64");
|
||||
const json = JSON.stringify([obj], (_, val) =>
|
||||
typeof val === "object" && val !== null && !Array.isArray(val)
|
||||
? Object.keys(val)
|
||||
.toSorted()
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = (val as Record<string, unknown>)[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
)
|
||||
: val,
|
||||
);
|
||||
return Buffer.from(json).toString("base64");
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import superjson from "superjson";
|
||||
import Transport from "winston-transport";
|
||||
import winston from "winston";
|
||||
|
||||
import type { RedisClient } from "../../redis/client";
|
||||
import { createRedisClient } from "../../redis/client";
|
||||
@@ -7,11 +7,7 @@ import { createRedisClient } from "../../redis/client";
|
||||
const messageSymbol = Symbol.for("message");
|
||||
const levelSymbol = Symbol.for("level");
|
||||
|
||||
//
|
||||
// Inherit from `winston-transport` so you can take advantage
|
||||
// of the base functionality and `.exceptions.handle()`.
|
||||
//
|
||||
export class RedisTransport extends Transport {
|
||||
export class RedisTransport extends winston.Transport {
|
||||
private redis: RedisClient | null = null;
|
||||
public static readonly publishChannel = "pubSub:logging";
|
||||
|
||||
|
||||
@@ -2357,6 +2357,9 @@
|
||||
"name": "Docker stats",
|
||||
"description": "Stats of your containers (This widget can only be added with administrator privileges)",
|
||||
"option": {
|
||||
"columns": {
|
||||
"label": "Columns to show"
|
||||
},
|
||||
"enableRowSorting": {
|
||||
"label": "Enable items sorting"
|
||||
},
|
||||
@@ -5096,7 +5099,8 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"internalServerError": "Failed to fetch Docker containers"
|
||||
"internalServerError": "Failed to fetch Docker containers",
|
||||
"noColumns": "Select at least one column in the widget settings"
|
||||
}
|
||||
},
|
||||
"kubernetes": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
|
||||
@@ -13,10 +13,12 @@ interface CalendarDayProps {
|
||||
disabled: boolean;
|
||||
rootWidth: number;
|
||||
rootHeight: number;
|
||||
onOpen: (close: () => void) => void;
|
||||
}
|
||||
|
||||
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
|
||||
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [pinned, setPinned] = useState(false);
|
||||
const { primaryColor } = useMantineTheme();
|
||||
const { ref, height } = useElementSize();
|
||||
const board = useRequiredBoard();
|
||||
@@ -29,6 +31,19 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
|
||||
|
||||
const isTooSmallForIndicators = height < 30;
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (disabled) return;
|
||||
onOpen(() => {
|
||||
setOpened(false);
|
||||
setPinned(false);
|
||||
});
|
||||
setOpened(true);
|
||||
}, [disabled, onOpen]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (!pinned) setOpened(false);
|
||||
}, [pinned]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="bottom"
|
||||
@@ -39,7 +54,10 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
|
||||
transitionProps={{
|
||||
transition: "pop",
|
||||
}}
|
||||
onChange={setOpened}
|
||||
onChange={(value) => {
|
||||
setOpened(value);
|
||||
if (!value) setPinned(false);
|
||||
}}
|
||||
opened={opened}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -59,10 +77,10 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
|
||||
setOpened((prev) => !prev);
|
||||
if (!disabled) setPinned((prev) => !prev);
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
|
||||
{date.getDate()}
|
||||
@@ -71,7 +89,15 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
|
||||
</Container>
|
||||
</Popover.Target>
|
||||
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
|
||||
<Popover.Dropdown maw="calc(100vw - 24px)" w={512} pe={4} pb={0} style={{ overflow: "hidden" }}>
|
||||
<Popover.Dropdown
|
||||
maw="calc(100vw - 24px)"
|
||||
w={512}
|
||||
pe={4}
|
||||
pb={0}
|
||||
style={{ overflow: "hidden" }}
|
||||
onMouseEnter={() => setOpened(true)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<CalendarEventList events={events} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useMantineTheme } from "@mantine/core";
|
||||
import { Calendar } from "@mantine/dates";
|
||||
@@ -16,11 +16,15 @@ import type { WidgetComponentProps } from "../definition";
|
||||
import { CalendarDay } from "./calender-day";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function CalendarWidget(props: WidgetComponentProps<"calendar">) {
|
||||
export default function CalendarWidget(
|
||||
props: WidgetComponentProps<"calendar">,
|
||||
) {
|
||||
const [month, setMonth] = useState(new Date());
|
||||
|
||||
if (props.integrationIds.length === 0) {
|
||||
return <CalendarBase {...props} events={[]} month={month} setMonth={setMonth} />;
|
||||
return (
|
||||
<CalendarBase {...props} events={[]} month={month} setMonth={setMonth} />
|
||||
);
|
||||
}
|
||||
|
||||
return <FetchCalendar month={month} setMonth={setMonth} {...props} />;
|
||||
@@ -31,7 +35,13 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
|
||||
setMonth: (date: Date) => void;
|
||||
}
|
||||
|
||||
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
|
||||
const FetchCalendar = ({
|
||||
month,
|
||||
setMonth,
|
||||
isEditMode,
|
||||
integrationIds,
|
||||
options,
|
||||
}: FetchCalendarProps) => {
|
||||
const input = {
|
||||
integrationIds,
|
||||
month: month.getMonth(),
|
||||
@@ -61,9 +71,20 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }:
|
||||
},
|
||||
});
|
||||
|
||||
const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]);
|
||||
const events = useMemo(
|
||||
() => data?.flatMap((item) => item.events) ?? [],
|
||||
[data],
|
||||
);
|
||||
|
||||
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
|
||||
return (
|
||||
<CalendarBase
|
||||
isEditMode={isEditMode}
|
||||
events={events}
|
||||
month={month}
|
||||
setMonth={setMonth}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface CalendarBaseProps {
|
||||
@@ -74,7 +95,13 @@ interface CalendarBaseProps {
|
||||
options: WidgetComponentProps<"calendar">["options"];
|
||||
}
|
||||
|
||||
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
|
||||
const CalendarBase = ({
|
||||
isEditMode,
|
||||
events,
|
||||
month,
|
||||
setMonth,
|
||||
options,
|
||||
}: CalendarBaseProps) => {
|
||||
const params = useParams();
|
||||
const locale = params.locale as string;
|
||||
const { firstDayOfWeek } = useSettings();
|
||||
@@ -85,6 +112,11 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
const isSmall = width < 256;
|
||||
|
||||
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
|
||||
const activeCloseRef = useRef<(() => void) | null>(null);
|
||||
const onDayOpen = useCallback((close: () => void) => {
|
||||
activeCloseRef.current?.();
|
||||
activeCloseRef.current = close;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
@@ -150,9 +182,14 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
const eventsForDate = normalizedEvents
|
||||
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
|
||||
.filter(
|
||||
(event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
|
||||
(event) =>
|
||||
event.metadata?.type !== "radarr" ||
|
||||
options.releaseType.includes(event.metadata.releaseType),
|
||||
)
|
||||
.toSorted((eventA, eventB) => eventA.startDate.getTime() - eventB.startDate.getTime());
|
||||
.toSorted(
|
||||
(eventA, eventB) =>
|
||||
eventA.startDate.getTime() - eventB.startDate.getTime(),
|
||||
);
|
||||
|
||||
return (
|
||||
<CalendarDay
|
||||
@@ -161,6 +198,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
disabled={isEditMode || eventsForDate.length === 0}
|
||||
rootWidth={width}
|
||||
rootHeight={height}
|
||||
onOpen={onDayOpen}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -198,7 +236,9 @@ export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => {
|
||||
splitEvents.push({
|
||||
...event,
|
||||
startDate: currentStart.toDate(),
|
||||
endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(),
|
||||
endDate: currentStart.endOf("day").isAfter(event.endDate)
|
||||
? event.endDate
|
||||
: currentStart.endOf("day").toDate(),
|
||||
});
|
||||
|
||||
currentStart = currentStart.add(1, "day").startOf("day");
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function DnsHoleControlsWidget({
|
||||
},
|
||||
{
|
||||
staleTime: 5 * 1000,
|
||||
refetchInterval: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function DnsHoleSummaryWidget({ options, integrationIds }: Widget
|
||||
},
|
||||
{
|
||||
staleTime: 5 * 1000,
|
||||
refetchInterval: 5 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { ActionIcon, Avatar, Badge, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Avatar, Badge, Center, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import type { IconProps } from "@tabler/icons-react";
|
||||
import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRotateClockwise } from "@tabler/icons-react";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
@@ -213,6 +213,15 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
|
||||
const columns = useMemo(() => createColumns(t), [t]);
|
||||
|
||||
const columnVisibility: MRT_VisibilityState = {
|
||||
name: options.columns.includes("name"),
|
||||
state: options.columns.includes("state"),
|
||||
host: options.columns.includes("host"),
|
||||
cpuUsage: options.columns.includes("cpuUsage"),
|
||||
memoryUsage: options.columns.includes("memoryUsage"),
|
||||
actions: options.columns.includes("actions"),
|
||||
};
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
columns,
|
||||
data: containers,
|
||||
@@ -233,6 +242,9 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }],
|
||||
density: "xs",
|
||||
},
|
||||
state: {
|
||||
columnVisibility,
|
||||
},
|
||||
mantinePaperProps: {
|
||||
flex: 1,
|
||||
withBorder: false,
|
||||
@@ -260,6 +272,13 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
},
|
||||
});
|
||||
|
||||
if (options.columns.length === 0)
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Text>{t("error.noColumns")}</Text>
|
||||
</Center>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack gap={0} h="100%" display="flex">
|
||||
<MantineReactTable table={table} />
|
||||
|
||||
@@ -12,10 +12,29 @@ const columnsList = [
|
||||
"memoryUsage",
|
||||
] as const satisfies (keyof RouterOutputs["docker"]["getContainers"]["containers"][number])[];
|
||||
|
||||
const allColumnsList = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const;
|
||||
|
||||
const columnTranslationKeyMap = {
|
||||
name: "docker.field.name.label",
|
||||
state: "docker.field.state.label",
|
||||
host: "docker.field.host.label",
|
||||
cpuUsage: "docker.field.stats.cpu.label",
|
||||
memoryUsage: "docker.field.stats.memory.label",
|
||||
actions: "docker.action.title",
|
||||
} as const satisfies Record<(typeof allColumnsList)[number], string>;
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
|
||||
icon: IconBrandDocker,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
columns: factory.multiSelect({
|
||||
defaultValue: [...allColumnsList],
|
||||
options: allColumnsList.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(columnTranslationKeyMap[value]),
|
||||
})),
|
||||
searchable: true,
|
||||
}),
|
||||
enableRowSorting: factory.switch({
|
||||
defaultValue: false,
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import "../widgets-common.css";
|
||||
import "./styles.css";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { MantineStyleProp } from "@mantine/core";
|
||||
@@ -577,7 +578,7 @@ export default function DownloadClientsWidget({
|
||||
enableColumnOrdering: isEditMode,
|
||||
enableRowVirtualization: true,
|
||||
rowVirtualizerOptions: { overscan: 5 },
|
||||
mantinePaperProps: { flex: 1, withBorder: false, shadow: undefined },
|
||||
mantinePaperProps: { flex: 1, withBorder: false, shadow: undefined, bg: "transparent" },
|
||||
mantineTableContainerProps: { style: { height: "100%" } },
|
||||
mantineTableProps: {
|
||||
className: "downloads-widget-table",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Make the MantineReactTable transparent so the board card's opacity-controlled
|
||||
background shows through. MRT defaults to var(--mantine-color-body) which is opaque. */
|
||||
.downloads-widget-table,
|
||||
.downloads-widget-table .mantine-Table-table,
|
||||
.downloads-widget-table .mantine-Table-thead,
|
||||
.downloads-widget-table .mantine-Table-tbody,
|
||||
.downloads-widget-table .mantine-Table-tfoot,
|
||||
.downloads-widget-table .mantine-Table-tr,
|
||||
.downloads-widget-table th,
|
||||
.downloads-widget-table td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@ catalog:
|
||||
"@dnd-kit/utilities": ^3.2.2
|
||||
"@drizzle-team/brocli": ^0.12.0
|
||||
"@extractus/feed-extractor": 7.2.1
|
||||
'@gfazioli/mantine-onboarding-tour': ^4.0.5
|
||||
"@gfazioli/mantine-onboarding-tour": ^4.0.5
|
||||
"@gitbeaker/rest": ^43.8.0
|
||||
"@homarr/gridstack": ^1.12.0
|
||||
"@homarr/node-unifi": ^2.6.0
|
||||
@@ -191,7 +191,7 @@ catalog:
|
||||
zod-form-data: ^3.0.2
|
||||
zod-validation-error: ^5.0.0
|
||||
catalogMode: strict
|
||||
nodeLinker: hoisted
|
||||
enableGlobalVirtualStore: true
|
||||
strictPeerDependencies: false
|
||||
engineStrict: true
|
||||
verifyDepsBeforeRun: warn
|
||||
|
||||
Reference in new issue
Block a user