Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def3ef8e15 | ||
|
|
2e709b102f | ||
|
|
875310f4d9 | ||
|
|
82064b2874 | ||
|
|
f1bed389e3 | ||
|
|
b6c817123f | ||
|
|
062ab1ad34 | ||
|
|
b7b7105f2d | ||
|
|
f0e20eb3da |
No files matched your search
@@ -5,7 +5,7 @@
|
||||
bottom: calc(100% - 10px);
|
||||
margin-bottom: 1px;
|
||||
z-index: 200;
|
||||
pointer-events: auto;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
transition:
|
||||
@@ -13,8 +13,10 @@
|
||||
transform 150ms ease;
|
||||
}
|
||||
|
||||
:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover) .wrapper,
|
||||
:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within) .wrapper {
|
||||
:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):hover)
|
||||
.wrapper,
|
||||
:global(.grid-stack-item[data-type="item"]:not([data-kind="app"]):focus-within)
|
||||
.wrapper {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
@@ -77,6 +79,8 @@
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metadataItem {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar, Group, Text, Tooltip } from "@mantine/core";
|
||||
import { Avatar, Text, Tooltip } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { widgetImports } from "@homarr/widgets";
|
||||
|
||||
import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import type { Translator } from "./widget-hover-metadata";
|
||||
import { collectWidgetHoverMetadata, resolveWidgetDisplayName } from "./widget-hover-metadata";
|
||||
import classes from "./widget-hover-overlay.module.css";
|
||||
|
||||
interface WidgetHoverOverlayProps {
|
||||
@@ -21,27 +18,17 @@ interface WidgetHoverOverlayProps {
|
||||
|
||||
export const WidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayProps) => {
|
||||
const [isEditMode] = useEditMode();
|
||||
const settings = useSettings();
|
||||
const t = useI18n();
|
||||
const { definition } = widgetImports[item.kind];
|
||||
const WidgetIcon = definition.icon;
|
||||
|
||||
const translate = ((key: string) => t(key as never)) as Translator;
|
||||
const displayName = resolveWidgetDisplayName(item, settings, translate);
|
||||
const displayName = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
|
||||
|
||||
const connectedIntegrations = (integrations ?? []).filter((integration) =>
|
||||
item.integrationIds.includes(integration.id),
|
||||
);
|
||||
const metadataItems = collectWidgetHoverMetadata(
|
||||
item,
|
||||
settings,
|
||||
connectedIntegrations.map((integration) => integration.kind),
|
||||
translate,
|
||||
);
|
||||
const hasIntegrations = connectedIntegrations.length > 0;
|
||||
const hasMetadata = metadataItems.length > 0;
|
||||
|
||||
if (isEditMode) {
|
||||
if (!isEditMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -55,31 +42,15 @@ export const WidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayPro
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{hasIntegrations && (
|
||||
{connectedIntegrations.length > 0 && (
|
||||
<Avatar.Group spacing="xs" className={classes.integrations}>
|
||||
{connectedIntegrations.map((integration) => (
|
||||
<Tooltip key={integration.id} label={integration.name} withArrow position="top">
|
||||
<Avatar
|
||||
src={getIconUrl(integration.kind)}
|
||||
size={20}
|
||||
radius="sm"
|
||||
className={classes.integrationAvatar}
|
||||
/>
|
||||
<Avatar src={getIconUrl(integration.kind)} size={18} radius="sm" className={classes.integrationAvatar} />
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
)}
|
||||
|
||||
{hasMetadata && (
|
||||
<Group className={classes.metadata} gap={4}>
|
||||
{metadataItems.map((metadataItem) => (
|
||||
<div key={`${metadataItem.label}-${metadataItem.value}`} className={classes.metadataItem}>
|
||||
<span className={classes.metadataLabel}>{metadataItem.label}</span>
|
||||
<span className={classes.metadataValue}>{metadataItem.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// it uses the sitemap.xml to generate the HomarrDocumentationPath type
|
||||
export type HomarrDocumentationPath =
|
||||
| "/about-us"
|
||||
| "/api-reference"
|
||||
| "/blog"
|
||||
| "/blog/2023/01/11/version0.11"
|
||||
| "/blog/2023/04/16/version0.12-more-widgets"
|
||||
@@ -42,9 +43,11 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/active-directory"
|
||||
| "/docs/tags/administration"
|
||||
| "/docs/tags/advanced"
|
||||
| "/docs/tags/ai"
|
||||
| "/docs/tags/analytics"
|
||||
| "/docs/tags/api"
|
||||
| "/docs/tags/apps"
|
||||
| "/docs/tags/automation"
|
||||
| "/docs/tags/background"
|
||||
| "/docs/tags/backup"
|
||||
| "/docs/tags/boards"
|
||||
@@ -55,9 +58,10 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/community"
|
||||
| "/docs/tags/configuration"
|
||||
| "/docs/tags/connections"
|
||||
| "/docs/tags/custom"
|
||||
| "/docs/tags/custom-widgets"
|
||||
| "/docs/tags/customization"
|
||||
| "/docs/tags/data-sources"
|
||||
| "/docs/tags/data"
|
||||
| "/docs/tags/database"
|
||||
| "/docs/tags/developer"
|
||||
| "/docs/tags/development"
|
||||
@@ -78,9 +82,11 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/integrations"
|
||||
| "/docs/tags/interface"
|
||||
| "/docs/tags/jobs"
|
||||
| "/docs/tags/jsx"
|
||||
| "/docs/tags/layout"
|
||||
| "/docs/tags/ldap"
|
||||
| "/docs/tags/management"
|
||||
| "/docs/tags/mcp"
|
||||
| "/docs/tags/media"
|
||||
| "/docs/tags/oidc"
|
||||
| "/docs/tags/open-collective"
|
||||
@@ -217,10 +223,12 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/uptime-kuma"
|
||||
| "/docs/management/api"
|
||||
| "/docs/management/apps"
|
||||
| "/docs/management/backup"
|
||||
| "/docs/management/boards"
|
||||
| "/docs/management/certificates"
|
||||
| "/docs/management/custom-widgets"
|
||||
| "/docs/management/integrations"
|
||||
| "/docs/management/mcp"
|
||||
| "/docs/management/media"
|
||||
| "/docs/management/search-engines"
|
||||
| "/docs/management/settings"
|
||||
|
||||
@@ -94,8 +94,8 @@ export interface BeszelSystemDetails {
|
||||
*
|
||||
* Units:
|
||||
* - CPU/GPU/memory/disk percentages: 0–100
|
||||
* - Memory/disk absolute values (mu, du, etc.): bytes
|
||||
* - Disk I/O (dr, dw): bytes/s
|
||||
* - Memory/disk absolute values (mu, du, m, d, etc.): GB
|
||||
* - Disk I/O (dr, dw): MB/s
|
||||
* - Network (ns, nr): bytes/s — all interfaces (including loopback, docker bridges)
|
||||
* - Bandwidth (b): bytes/s [sent, recv] — public interfaces only. Prefer b over ns/nr for user-facing charts
|
||||
*/
|
||||
@@ -110,35 +110,35 @@ export interface BeszelSystemStats {
|
||||
cpus?: number[];
|
||||
/** load average [1m, 5m, 15m] */
|
||||
la?: [number, number, number];
|
||||
/** total memory (bytes) */
|
||||
/** total memory (GB) */
|
||||
m: number;
|
||||
/** memory used (bytes) */
|
||||
/** memory used (GB) */
|
||||
mu: number;
|
||||
/** memory usage (%) */
|
||||
mp: number;
|
||||
/** memory buffer/cache (bytes) */
|
||||
/** memory buffer/cache (GB) */
|
||||
mb: number;
|
||||
/** memory max used (bytes) */
|
||||
/** memory max used (GB) */
|
||||
mm?: number;
|
||||
/** memory zfs arc (bytes) */
|
||||
/** memory zfs arc (GB) */
|
||||
mz?: number;
|
||||
/** total swap (bytes) */
|
||||
/** total swap (GB) */
|
||||
s: number;
|
||||
/** swap used (bytes) */
|
||||
/** swap used (GB) */
|
||||
su: number;
|
||||
/** total disk (bytes) */
|
||||
/** total disk (GB) */
|
||||
d: number;
|
||||
/** disk used (bytes) */
|
||||
/** disk used (GB) */
|
||||
du: number;
|
||||
/** disk usage (%) */
|
||||
dp: number;
|
||||
/** disk read (bytes/s) */
|
||||
/** disk read (MB/s) */
|
||||
dr: number;
|
||||
/** disk write (bytes/s) */
|
||||
/** disk write (MB/s) */
|
||||
dw: number;
|
||||
/** disk read max (bytes/s) */
|
||||
/** disk read max (MB/s) */
|
||||
drm?: number;
|
||||
/** disk write max (bytes/s) */
|
||||
/** disk write max (MB/s) */
|
||||
dwm?: number;
|
||||
/** disk IOPS [read, write] */
|
||||
dio?: [number, number];
|
||||
@@ -236,13 +236,14 @@ export interface BeszelContainer {
|
||||
* One entry per container in a BeszelContainerStatsRecord.
|
||||
*
|
||||
* Network fields: b (bandwidth) is preferred when available; fall back to ns/nr.
|
||||
* Units: m = MB, c = %, b/ns/nr = bytes/s
|
||||
*/
|
||||
export interface BeszelContainerStats {
|
||||
/** container name */
|
||||
n: string;
|
||||
/** CPU usage (%) */
|
||||
c: number;
|
||||
/** memory usage (bytes) */
|
||||
/** memory usage (MB) */
|
||||
m: number;
|
||||
/** network sent (bytes/s) — legacy, use b when available */
|
||||
ns?: number;
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
} from "../../beszel/beszel-types";
|
||||
|
||||
const GB = 1024 * 1024 * 1024;
|
||||
const MB = 1024 * 1024;
|
||||
const KB = 1024;
|
||||
|
||||
interface MockSystemDef {
|
||||
@@ -144,26 +143,27 @@ function generateSystemStatsSeries(count: number, sys: MockSystemDef): BeszelSys
|
||||
const netSendWalk = smoothWalk(500, 200, count, 10, 8000);
|
||||
const netRecvWalk = smoothWalk(800, 300, count, 10, 12000);
|
||||
|
||||
const memTotal = sys.mem;
|
||||
const diskTotal = rand(200, 2000) * GB;
|
||||
|
||||
return cpuWalk.map((cpu, i) => {
|
||||
const memPct = memPctWalk[i] ?? sys.baseMem;
|
||||
const diskPct = diskPctWalk[i] ?? sys.baseDisk;
|
||||
const memTotalGB = sys.mem / GB;
|
||||
const diskTotalGB = diskTotal / GB;
|
||||
return {
|
||||
cpu,
|
||||
la: [cpu / 20 + rand(0, 0.5), cpu / 25 + rand(0, 0.3), cpu / 30 + rand(0, 0.2)] as [number, number, number],
|
||||
m: memTotal,
|
||||
mu: (memTotal * memPct) / 100,
|
||||
m: memTotalGB,
|
||||
mu: (memTotalGB * memPct) / 100,
|
||||
mp: memPct,
|
||||
mb: rand(0.5, 4) * GB,
|
||||
s: 4 * GB,
|
||||
su: rand(0, 2) * GB,
|
||||
d: diskTotal,
|
||||
du: (diskTotal * diskPct) / 100,
|
||||
mb: rand(0.5, 4),
|
||||
s: 4,
|
||||
su: rand(0, 2),
|
||||
d: diskTotalGB,
|
||||
du: (diskTotalGB * diskPct) / 100,
|
||||
dp: diskPct,
|
||||
dr: (diskReadWalk[i] ?? 5) * MB,
|
||||
dw: (diskWriteWalk[i] ?? 3) * MB,
|
||||
dr: diskReadWalk[i] ?? 5,
|
||||
dw: diskWriteWalk[i] ?? 3,
|
||||
ns: (netSendWalk[i] ?? 500) * KB,
|
||||
nr: (netRecvWalk[i] ?? 800) * KB,
|
||||
b: [(netSendWalk[i] ?? 500) * KB * 0.7, (netRecvWalk[i] ?? 800) * KB * 0.7] as [number, number],
|
||||
@@ -183,7 +183,7 @@ function generateContainerStatsSeries(count: number, profiles: typeof containerP
|
||||
walks.map(({ profile, cpuWalk, memWalk, netWalk }) => ({
|
||||
n: profile.name,
|
||||
c: cpuWalk[i] ?? profile.cpuBase,
|
||||
m: (memWalk[i] ?? profile.memMB) * MB,
|
||||
m: memWalk[i] ?? profile.memMB,
|
||||
b: [(netWalk[i] ?? profile.netKB) * KB * 0.4, (netWalk[i] ?? profile.netKB) * KB * 0.6] as [number, number],
|
||||
})),
|
||||
);
|
||||
@@ -264,13 +264,15 @@ export class BeszelMockService {
|
||||
const sys = resolveSystem(systemId);
|
||||
const intervalMinutes = typeToMinutes[type] ?? 1;
|
||||
const statsSeries = generateSystemStatsSeries(perPage, sys);
|
||||
return buildTimeSeries(
|
||||
const result = buildTimeSeries(
|
||||
systemId,
|
||||
perPage,
|
||||
intervalMinutes,
|
||||
type,
|
||||
statsSeries.map((stats) => ({ stats })),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getContainerStatsAsync(
|
||||
@@ -284,13 +286,15 @@ export class BeszelMockService {
|
||||
.map((i) => containerProfiles[i])
|
||||
.filter(Boolean) as typeof containerProfiles;
|
||||
const series = generateContainerStatsSeries(perPage, selectedProfiles);
|
||||
return buildTimeSeries(
|
||||
const result = buildTimeSeries(
|
||||
systemId,
|
||||
perPage,
|
||||
intervalMinutes,
|
||||
type,
|
||||
series.map((stats) => ({ stats })),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getAlertsAsync(): Promise<BeszelAlert[]> {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
object-fit: contain;
|
||||
scale: 0.8;
|
||||
transition: scale 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.appWithUrl:hover > .appIcon {
|
||||
|
||||
@@ -63,23 +63,23 @@ export default function BeszelSystemStatsWidget({
|
||||
{ staleTime: 30 * 1000 },
|
||||
);
|
||||
|
||||
// BeszelSystemStats fields are documented in beszel-types.ts.
|
||||
// mu/du = bytes, dr/dw = bytes/s, b = public bandwidth [sent,recv] bytes/s, ns/nr = all-interface bytes/s
|
||||
const BYTES_TO_GB = 1024 * 1024 * 1024;
|
||||
// BeszelSystemStats fields: storage values (m, mu, mb, d, du, s, su) are already in GB.
|
||||
// Disk I/O (dr, dw) is in MB/s — scale to bytes/s for display formatters.
|
||||
// Network (ns, nr, b) is in bytes/s. Container memory (m) is in MB.
|
||||
|
||||
const cpuMap = useCallback((s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }), [t]);
|
||||
const memoryMap = useCallback(
|
||||
(s: { mu: number; mb: number }) => ({
|
||||
[t("chart.memory.series")]: s.mu / BYTES_TO_GB,
|
||||
[t("chart.memory.cache")]: (s.mb ?? 0) / BYTES_TO_GB,
|
||||
[t("chart.memory.series")]: s.mu,
|
||||
[t("chart.memory.cache")]: s.mb ?? 0,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
const diskMap = useCallback((s: { du: number }) => ({ [t("chart.disk.series")]: s.du / BYTES_TO_GB }), [t]);
|
||||
const diskMap = useCallback((s: { du: number }) => ({ [t("chart.disk.series")]: s.du }), [t]);
|
||||
const diskIOMap = useCallback(
|
||||
(s: { dr?: number; dw?: number }) => ({
|
||||
[t("chart.diskIO.read")]: s.dr ?? 0,
|
||||
[t("chart.diskIO.write")]: s.dw ?? 0,
|
||||
[t("chart.diskIO.read")]: (s.dr ?? 0) * (1024 * 1024),
|
||||
[t("chart.diskIO.write")]: (s.dw ?? 0) * (1024 * 1024),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
@@ -46,7 +46,8 @@ export const BeszelAreaChart = ({ yAxisFormatter, yAxisDomain, yAxisProps, style
|
||||
withDots={false}
|
||||
withXAxis
|
||||
withYAxis
|
||||
style={{ minWidth: 0, ...style }}
|
||||
w="100%"
|
||||
style={{ minWidth: 0, minHeight: 0, ...style }}
|
||||
yAxisProps={{
|
||||
...yAxisBase,
|
||||
...(yAxisDomain ? { domain: yAxisDomain } : {}),
|
||||
@@ -83,10 +84,12 @@ export const useContainerNames = (containerStats: BeszelContainerStatsRecord[] |
|
||||
|
||||
type ContainerExtractor = (container: BeszelContainerStatsRecord["stats"][number] | undefined) => number;
|
||||
|
||||
// c = CPU (%), m = memory (bytes), b = bandwidth [sent,recv] (bytes/s), ns/nr = legacy net (bytes/s)
|
||||
// c = CPU (%), m = memory (MB), b = bandwidth [sent,recv] (bytes/s), ns/nr = legacy net (bytes/s)
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
const defaultContainerExtractors: Record<string, ContainerExtractor> = {
|
||||
cpu: (c) => c?.c ?? 0,
|
||||
memory: (c) => c?.m ?? 0,
|
||||
memory: (c) => (c?.m ?? 0) * MB,
|
||||
network: (c) => (c?.b ? c.b[0] + c.b[1] : (c?.ns ?? 0) + (c?.nr ?? 0)),
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user