Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d410b43f2 | ||
|
|
aca3b5d861 |
No files matched your search
@@ -1,30 +1,27 @@
|
|||||||
---
|
---
|
||||||
title: 'App'
|
title: "App"
|
||||||
description: 'Embeds an app into the board.'
|
description: "Embeds an app into the board."
|
||||||
hide_title: true
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
import { AddingWidget } from '@site/src/components/widgets/adding';
|
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||||
import { WidgetConfig } from '@site/src/components/widgets/configuration';
|
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||||
import { appWidget } from '.';
|
import { appWidget } from ".";
|
||||||
|
|
||||||
|
<WidgetHeader widget={appWidget} categories={["Links", "Apps"]} />
|
||||||
<WidgetHeader
|
|
||||||
widget={appWidget}
|
|
||||||
categories={['Links', 'Apps']}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board.
|
The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board.
|
||||||
It also supports a status check to indicate whether Homarr can reach the application or website.
|
It also supports a status check to indicate whether Homarr can reach the application or website.
|
||||||
|
In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
import githubApp from './img/github.png';
|
import githubApp from "./img/github.png";
|
||||||
import jellyseerrApp from './img/jellyseerr-description-tooltip.png';
|
import jellyseerrApp from "./img/jellyseerr-description-tooltip.png";
|
||||||
import piHoleApp from './img/pi-hole-ping-icon.png';
|
import piHoleApp from "./img/pi-hole-ping-icon.png";
|
||||||
|
|
||||||
<div className='flex gap-1'>
|
<div className="flex gap-1">
|
||||||
<img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} />
|
<img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} />
|
||||||
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
|
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
|
||||||
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
|
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
|
||||||
@@ -34,4 +31,5 @@ import piHoleApp from './img/pi-hole-ping-icon.png';
|
|||||||
<AddingWidget />
|
<AddingWidget />
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
<WidgetConfig configuration={appWidget.configuration} />
|
<WidgetConfig configuration={appWidget.configuration} />
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: 'Bookmarks'
|
title: "Bookmarks"
|
||||||
description: 'Displays multiple app links'
|
description: "Displays multiple app links"
|
||||||
hide_title: true
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
import { AddingWidget } from '@site/src/components/widgets/adding';
|
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||||
import { WidgetConfig } from '@site/src/components/widgets/configuration';
|
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||||
import { bookmarksWidget } from '.';
|
import { bookmarksWidget } from ".";
|
||||||
|
|
||||||
|
<WidgetHeader widget={bookmarksWidget} categories={["Links", "Apps"]} />
|
||||||
<WidgetHeader
|
|
||||||
widget={bookmarksWidget}
|
|
||||||
categories={['Links', 'Apps']}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The bookmarks widget allows you to display multiple app links in a single widget.
|
The bookmarks widget allows you to display multiple app links in a single widget.
|
||||||
You can add links to your favorite applications, websites, or any other resources you want quick access to.
|
You can add links to your favorite applications, websites, or any other resources you want quick access to.
|
||||||
|
In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
import gridIconsOnly from './img/grid-icons-only.png';
|
import gridIconsOnly from "./img/grid-icons-only.png";
|
||||||
import gridWithTitle from './img/grid-with-title.png';
|
import gridWithTitle from "./img/grid-with-title.png";
|
||||||
import verticalListAll from './img/vertical-list-all.png';
|
import verticalListAll from "./img/vertical-list-all.png";
|
||||||
import vericalListSimple from './img/vertical-list-simple.png';
|
import vericalListSimple from "./img/vertical-list-simple.png";
|
||||||
|
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
@@ -33,12 +30,13 @@ import vericalListSimple from './img/vertical-list-simple.png';
|
|||||||
|
|
||||||
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
|
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
|
||||||
<img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} />
|
<img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} />
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
### Adding the widget
|
### Adding the widget
|
||||||
|
|
||||||
<AddingWidget />
|
<AddingWidget />
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
<WidgetConfig configuration={bookmarksWidget.configuration} />
|
<WidgetConfig configuration={bookmarksWidget.configuration} />
|
||||||
@@ -178,35 +178,37 @@ export const BoardSettingsForm = ({ board, permissions, hasFullAccess, hideVisib
|
|||||||
const isPending = savePartialSettings.isPending || saveLayouts.isPending;
|
const isPending = savePartialSettings.isPending || saveLayouts.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
<Stack gap="xl">
|
||||||
<Stack gap="xl">
|
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||||
<GeneralSettingsContent board={board} form={form} />
|
<Stack gap="xl">
|
||||||
<LayoutSettingsContent form={form} />
|
<GeneralSettingsContent board={board} form={form} />
|
||||||
<BackgroundSettingsContent form={form} />
|
<LayoutSettingsContent form={form} />
|
||||||
<ColorSettingsContent form={form} />
|
<BackgroundSettingsContent form={form} />
|
||||||
<CustomCssSettingsContent form={form} />
|
<ColorSettingsContent form={form} />
|
||||||
<BehaviorSettingsContent form={form} />
|
<CustomCssSettingsContent form={form} />
|
||||||
|
<BehaviorSettingsContent form={form} />
|
||||||
|
|
||||||
{hasFullAccess && (
|
{form.isDirty() && (
|
||||||
<SectionCard title={tSection("access.title")}>
|
<UnsavedChangesBar>
|
||||||
<BoardAccessSettings board={board} initialPermissions={permissions} />
|
<Button type="button" disabled={isPending} variant="default" onClick={handleDiscard}>
|
||||||
</SectionCard>
|
{t("common.action.discard")}
|
||||||
)}
|
</Button>
|
||||||
|
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
|
||||||
|
{t("common.action.saveChanges")}
|
||||||
|
</Button>
|
||||||
|
</UnsavedChangesBar>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
{hasFullAccess && <DangerZoneSettingsContent hideVisibility={hideVisibility} />}
|
{hasFullAccess && (
|
||||||
|
<SectionCard title={tSection("access.title")}>
|
||||||
|
<BoardAccessSettings board={board} initialPermissions={permissions} />
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
|
||||||
{form.isDirty() && (
|
{hasFullAccess && <DangerZoneSettingsContent hideVisibility={hideVisibility} />}
|
||||||
<UnsavedChangesBar>
|
</Stack>
|
||||||
<Button type="button" disabled={isPending} variant="default" onClick={handleDiscard}>
|
|
||||||
{t("common.action.discard")}
|
|
||||||
</Button>
|
|
||||||
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
|
|
||||||
{t("common.action.saveChanges")}
|
|
||||||
</Button>
|
|
||||||
</UnsavedChangesBar>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,13 @@ interface WidgetContextMenuProps {
|
|||||||
|
|
||||||
export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => {
|
export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const [isEditMode] = useEditMode();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
if (!session || !settings.enableRightClickOnWidgets) return <>{children}</>;
|
if (!session || !settings.enableRightClickOnWidgets) return <>{children}</>;
|
||||||
|
const isLinkWidget = item.kind === "app" || item.kind === "bookmarks";
|
||||||
|
// Keep the Homarr menu in edit mode so these widgets can still be edited or removed.
|
||||||
|
if (isLinkWidget && !isEditMode) return <>{children}</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}>
|
<WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}>
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ interface UseCssVariableConfiguration {
|
|||||||
isDynamic: boolean;
|
isDynamic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minimumResizeWidthDelta = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook is used to configure the css variables for the gridstack
|
* This hook is used to configure the css variables for the gridstack
|
||||||
* Those css variables are used to define the size of the gridstack items
|
* Those css variables are used to define the size of the gridstack items
|
||||||
@@ -319,9 +321,30 @@ const useCssVariableConfiguration = ({
|
|||||||
columnCount,
|
columnCount,
|
||||||
isDynamic,
|
isDynamic,
|
||||||
}: UseCssVariableConfiguration) => {
|
}: UseCssVariableConfiguration) => {
|
||||||
|
const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
|
||||||
|
|
||||||
const onResize = useCallback(() => {
|
const onResize = useCallback(() => {
|
||||||
if (!wrapperRef.current) return;
|
if (!wrapperRef.current) return;
|
||||||
if (!gridRef.current) return;
|
if (!gridRef.current) return;
|
||||||
|
|
||||||
|
const currentWidth = wrapperRef.current.clientWidth;
|
||||||
|
const currentHeight = wrapperRef.current.clientHeight;
|
||||||
|
const prevSize = prevSizeRef.current;
|
||||||
|
|
||||||
|
// Only skip recalculation when the width change is insignificant
|
||||||
|
// and, for dynamic sections, the height is unchanged. This prevents infinite
|
||||||
|
// feedback loops when the vertical scrollbar appears/disappears, causing
|
||||||
|
// clientWidth to oscillate by ~15px, while still reacting to height changes.
|
||||||
|
if (
|
||||||
|
prevSize !== null &&
|
||||||
|
Math.abs(currentWidth - prevSize.width) < minimumResizeWidthDelta &&
|
||||||
|
(!isDynamic || currentHeight === prevSize.height)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSizeRef.current = { width: currentWidth, height: currentHeight };
|
||||||
|
|
||||||
handleResizeChange(
|
handleResizeChange(
|
||||||
wrapperRef.current,
|
wrapperRef.current,
|
||||||
gridRef.current,
|
gridRef.current,
|
||||||
|
|||||||
@@ -37,9 +37,16 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
|
|||||||
const systemInfo = await this.getSystemInformationAsync();
|
const systemInfo = await this.getSystemInformationAsync();
|
||||||
|
|
||||||
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
|
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
|
||||||
const cpuCount = systemInfo.info.cpu.cores;
|
const cpuCount = systemInfo.metrics.cpu.cpus.length;
|
||||||
|
|
||||||
|
// Each entry in cpus[] is one logical CPU (thread). Dividing the summed per-thread
|
||||||
|
// utilization by the thread count (not physical cores) gives the correct average and
|
||||||
|
// avoids over-reporting on SMT systems. Guard against zero to avoid Infinity/NaN.
|
||||||
|
let cpuUtilizationNormalized = 0;
|
||||||
|
if (cpuCount > 0) {
|
||||||
|
cpuUtilizationNormalized = cpuUtilization / cpuCount;
|
||||||
|
}
|
||||||
|
|
||||||
// We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays.
|
|
||||||
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
|
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
|
||||||
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
|
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
|
||||||
const uptime = dayjs(systemInfo.info.os.uptime);
|
const uptime = dayjs(systemInfo.info.os.uptime);
|
||||||
@@ -47,7 +54,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
|
|||||||
return {
|
return {
|
||||||
version: systemInfo.info.os.release,
|
version: systemInfo.info.os.release,
|
||||||
cpuModelName: systemInfo.info.cpu.brand,
|
cpuModelName: systemInfo.info.cpu.brand,
|
||||||
cpuUtilization: cpuUtilization / cpuCount,
|
cpuUtilization: cpuUtilizationNormalized,
|
||||||
memUsedInBytes: usedMemory,
|
memUsedInBytes: usedMemory,
|
||||||
memAvailableInBytes: totalMemory - usedMemory,
|
memAvailableInBytes: totalMemory - usedMemory,
|
||||||
uptime: dayjs().diff(uptime, "seconds"),
|
uptime: dayjs().diff(uptime, "seconds"),
|
||||||
|
|||||||
Reference in new issue
Block a user