Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def3ef8e15 | ||
|
|
2e709b102f | ||
|
|
875310f4d9 | ||
|
|
82064b2874 | ||
|
|
f1bed389e3 | ||
|
|
b6c817123f | ||
|
|
062ab1ad34 | ||
|
|
b7b7105f2d | ||
|
|
f0e20eb3da | ||
|
|
db8440bddf |
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[]> {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1419
-232
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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