Merge branch 'dev' into gateway-api
This commit is contained in:
177 files changed
+5281
-1398
No files matched your search
@@ -7,7 +7,13 @@ import ErrorBoundary from "components/errorboundry";
|
||||
import List from "components/bookmarks/list";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function BookmarksGroup({ bookmarks, layout, disableCollapse, groupsInitiallyCollapsed }) {
|
||||
export default function BookmarksGroup({
|
||||
bookmarks,
|
||||
layout,
|
||||
disableCollapse,
|
||||
groupsInitiallyCollapsed,
|
||||
bookmarksStyle,
|
||||
}) {
|
||||
const panel = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -18,9 +24,9 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse, gro
|
||||
<div
|
||||
key={bookmarks.name}
|
||||
className={classNames(
|
||||
"bookmark-group",
|
||||
"bookmark-group flex-1 overflow-hidden",
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
|
||||
layout?.header === false ? "flex-1 px-1 -my-1 overflow-hidden" : "flex-1 p-1 overflow-hidden",
|
||||
layout?.header === false ? "px-1" : "p-1 pb-0",
|
||||
)}
|
||||
>
|
||||
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
|
||||
@@ -64,7 +70,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse, gro
|
||||
>
|
||||
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
|
||||
<ErrorBoundary>
|
||||
<List bookmarks={bookmarks.bookmarks} layout={layout} />
|
||||
<List bookmarks={bookmarks.bookmarks} layout={layout} bookmarksStyle={bookmarksStyle} />
|
||||
</ErrorBoundary>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
|
||||
@@ -4,12 +4,17 @@ import classNames from "classnames";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ bookmark }) {
|
||||
export default function Item({ bookmark, iconOnly = false }) {
|
||||
const description = bookmark.description ?? new URL(bookmark.href).hostname;
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<li key={bookmark.name} id={bookmark.id} className="bookmark" data-name={bookmark.name}>
|
||||
<li
|
||||
key={bookmark.name}
|
||||
id={bookmark.id}
|
||||
className={classNames("bookmark", iconOnly && "grid")}
|
||||
data-name={bookmark.name}
|
||||
>
|
||||
<a
|
||||
href={bookmark.href}
|
||||
title={bookmark.name}
|
||||
@@ -17,25 +22,37 @@ export default function Item({ bookmark }) {
|
||||
target={bookmark.target ?? settings.target ?? "_blank"}
|
||||
className={classNames(
|
||||
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
|
||||
"block w-full text-left cursor-pointer transition-all h-15 mb-3 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
|
||||
"text-left cursor-pointer transition-all rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
|
||||
iconOnly ? "h-[60px] w-[60px] grid" : "block w-full h-15 mb-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md bookmark-icon">
|
||||
{iconOnly ? (
|
||||
<div className="flex items-center justify-center text-theme-700 hover:text-theme-700 dark:text-theme-200 text-xl font-medium rounded-md bookmark-icon py-0.5">
|
||||
{bookmark.icon && (
|
||||
<div className="flex-shrink-0 w-5 h-5">
|
||||
<div className="w-7 h-7">
|
||||
<ResolvedIcon icon={bookmark.icon} alt={bookmark.abbr} />
|
||||
</div>
|
||||
)}
|
||||
{!bookmark.icon && bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden flex items-center justify-between rounded-r-md bookmark-text">
|
||||
<div className="pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||
<div className="shrink truncate px-2 py-2 text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
{description}
|
||||
) : (
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md bookmark-icon">
|
||||
{bookmark.icon && (
|
||||
<div className="flex-shrink-0 w-5 h-5">
|
||||
<ResolvedIcon icon={bookmark.icon} alt={bookmark.abbr} />
|
||||
</div>
|
||||
)}
|
||||
{!bookmark.icon && bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden flex items-center justify-between rounded-r-md bookmark-text">
|
||||
<div className="pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||
<div className="shrink truncate px-2 py-2 text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -4,16 +4,21 @@ import { columnMap } from "../../utils/layout/columns";
|
||||
|
||||
import Item from "components/bookmarks/item";
|
||||
|
||||
export default function List({ bookmarks, layout }) {
|
||||
export default function List({ bookmarks, layout, bookmarksStyle }) {
|
||||
let classes = layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col bookmark-list";
|
||||
const style = {};
|
||||
if (layout?.iconsOnly || bookmarksStyle === "icons") {
|
||||
classes = "grid gap-2 bookmark-list";
|
||||
style.gridTemplateColumns = "repeat(auto-fill, minmax(60px, 1fr))";
|
||||
}
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
|
||||
"mt-3 bookmark-list",
|
||||
)}
|
||||
>
|
||||
<ul className={classNames(classes, "mb-2", layout?.header === false ? "" : "mt-3")} style={style}>
|
||||
{bookmarks.map((bookmark) => (
|
||||
<Item key={`${bookmark.name}-${bookmark.href}`} bookmark={bookmark} />
|
||||
<Item
|
||||
key={`${bookmark.name}-${bookmark.href}`}
|
||||
bookmark={bookmark}
|
||||
iconOnly={layout?.iconsOnly || bookmarksStyle === "icons"}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -14,8 +14,10 @@ export default class ErrorBoundary extends React.Component {
|
||||
});
|
||||
|
||||
// You can also log error messages to an error reporting service here
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error, errorInfo);
|
||||
if (error || errorInfo) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("component error: %s, info: %s", error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
if (searchString.length === 0) setResults([]);
|
||||
if (searchString.trim().length === 0) setResults([]);
|
||||
else {
|
||||
let newResults = servicesAndBookmarks.filter((r) => {
|
||||
const nameMatch = r.name.toLowerCase().includes(searchString);
|
||||
|
||||
@@ -101,7 +101,26 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
|
||||
const iconName = icon.replace(".svg", "");
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/${iconName}.svg`}
|
||||
src={`https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/${iconName}.svg`}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
objectFit: "contain",
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (icon.endsWith(".webp")) {
|
||||
const iconName = icon.replace(".webp", "");
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/webp/${iconName}.webp`}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
@@ -119,7 +138,7 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
|
||||
const iconName = icon.replace(".png", "");
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
|
||||
src={`https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/${iconName}.png`}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
|
||||
@@ -3,17 +3,19 @@ import classNames from "classnames";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||
|
||||
import { columnMap } from "../../utils/layout/columns";
|
||||
|
||||
import List from "components/services/list";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function ServicesGroup({
|
||||
group,
|
||||
services,
|
||||
layout,
|
||||
fiveColumns,
|
||||
disableCollapse,
|
||||
useEqualHeights,
|
||||
groupsInitiallyCollapsed,
|
||||
isSubgroup,
|
||||
}) {
|
||||
const panel = useRef();
|
||||
|
||||
@@ -21,14 +23,18 @@ export default function ServicesGroup({
|
||||
if (layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) panel.current.style.height = `0`;
|
||||
}, [layout, groupsInitiallyCollapsed]);
|
||||
|
||||
let groupPadding = layout?.header === false ? "px-1" : "p-1 pb-0";
|
||||
if (isSubgroup) groupPadding = "";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={services.name}
|
||||
key={group.name}
|
||||
className={classNames(
|
||||
"services-group",
|
||||
"services-group flex-1",
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
|
||||
layout?.style !== "row" && fiveColumns ? "3xl:basis-1/5" : "",
|
||||
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
|
||||
groupPadding,
|
||||
isSubgroup ? "subgroup" : "",
|
||||
)}
|
||||
>
|
||||
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
|
||||
@@ -42,7 +48,7 @@ export default function ServicesGroup({
|
||||
</div>
|
||||
)}
|
||||
<h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium service-group-name">
|
||||
{services.name}
|
||||
{group.name}
|
||||
</h2>
|
||||
<MdKeyboardArrowDown
|
||||
className={classNames(
|
||||
@@ -74,7 +80,33 @@ export default function ServicesGroup({
|
||||
}}
|
||||
>
|
||||
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
|
||||
<List group={group} services={services.services} layout={layout} useEqualHeights={useEqualHeights} />
|
||||
<List
|
||||
groupName={group.name}
|
||||
services={group.services}
|
||||
layout={layout}
|
||||
useEqualHeights={useEqualHeights}
|
||||
header={layout?.header !== false}
|
||||
/>
|
||||
{group.groups?.length > 0 && (
|
||||
<div
|
||||
className={`grid ${
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col"
|
||||
} gap-2`}
|
||||
>
|
||||
{group.groups.map((subgroup) => (
|
||||
<ServicesGroup
|
||||
key={subgroup.name}
|
||||
group={subgroup}
|
||||
layout={layout?.[subgroup.name]}
|
||||
fiveColumns={fiveColumns}
|
||||
disableCollapse={disableCollapse}
|
||||
useEqualHeights={useEqualHeights}
|
||||
groupsInitiallyCollapsed={groupsInitiallyCollapsed}
|
||||
isSubgroup
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ import Kubernetes from "widgets/kubernetes/component";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ service, group, useEqualHeights }) {
|
||||
export default function Item({ service, groupName, useEqualHeights }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const showStats = service.showStats === false ? false : settings.showStats;
|
||||
@@ -86,18 +86,18 @@ export default function Item({ service, group, useEqualHeights }) {
|
||||
<div
|
||||
className={`absolute top-0 right-0 flex flex-row justify-end ${
|
||||
statusStyle === "dot" ? "gap-0" : "gap-2 mr-2"
|
||||
} z-30 service-tags`}
|
||||
} z-10 service-tags`}
|
||||
>
|
||||
{service.ping && (
|
||||
<div className="flex-shrink-0 flex items-center justify-center service-tag service-ping">
|
||||
<Ping group={group} service={service.name} style={statusStyle} />
|
||||
<Ping groupName={groupName} serviceName={service.name} style={statusStyle} />
|
||||
<span className="sr-only">Ping status</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.siteMonitor && (
|
||||
<div className="flex-shrink-0 flex items-center justify-center service-tag service-site-monitor">
|
||||
<SiteMonitor group={group} service={service.name} style={statusStyle} />
|
||||
<SiteMonitor groupName={groupName} serviceName={service.name} style={statusStyle} />
|
||||
<span className="sr-only">Site monitor status</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -154,7 +154,9 @@ export default function Item({ service, group, useEqualHeights }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.widget && <Widget service={service} />}
|
||||
{service.widgets.map((widget) => (
|
||||
<Widget widget={widget} service={service} key={widget.index} />
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -4,19 +4,20 @@ import { columnMap } from "../../utils/layout/columns";
|
||||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ group, services, layout, useEqualHeights }) {
|
||||
export default function List({ groupName, services, layout, useEqualHeights, header }) {
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
|
||||
"mt-3 services-list",
|
||||
header ? "mt-3" : "",
|
||||
"services-list",
|
||||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item
|
||||
key={[service.container, service.app, service.name].filter((s) => s).join("-")}
|
||||
service={service}
|
||||
group={group}
|
||||
groupName={groupName}
|
||||
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Ping({ group, service, style }) {
|
||||
export default function Ping({ groupName, serviceName, style }) {
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ group, service }).toString()}`, {
|
||||
const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ groupName, serviceName }).toString()}`, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function SiteMonitor({ group, service, style }) {
|
||||
export default function SiteMonitor({ groupName, serviceName, style }) {
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ group, service }).toString()}`, {
|
||||
const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ groupName, serviceName }).toString()}`, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,22 +3,24 @@ import { useTranslation } from "next-i18next";
|
||||
import ErrorBoundary from "components/errorboundry";
|
||||
import components from "widgets/components";
|
||||
|
||||
export default function Widget({ service }) {
|
||||
export default function Widget({ widget, service }) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const ServiceWidget = components[service.widget.type];
|
||||
const ServiceWidget = components[widget.type];
|
||||
|
||||
const fullService = Object.apply({}, service);
|
||||
fullService.widget = widget;
|
||||
if (ServiceWidget) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ServiceWidget service={service} />
|
||||
<ServiceWidget service={fullService} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1 service-missing">
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: widget.type })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import useSWR from "swr";
|
||||
import { FaNetworkWired } from "react-icons/fa";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Resource from "../widget/resource";
|
||||
import Error from "../widget/error";
|
||||
|
||||
export default function Network({ options, refresh = 1500 }) {
|
||||
const { t } = useTranslation();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (options.network === true) options.network = "default";
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=network&interfaceName=${options.network}`, {
|
||||
refreshInterval: refresh,
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return <Error />;
|
||||
}
|
||||
|
||||
if (!data || !data.network || !data.network.rx_sec || !data.network.tx_sec) {
|
||||
return (
|
||||
<Resource
|
||||
icon={FaNetworkWired}
|
||||
value="- ↑"
|
||||
label="- ↓"
|
||||
expandedValue="- ↑"
|
||||
expandedLabel="- ↓"
|
||||
percentage="0"
|
||||
wide
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Resource
|
||||
icon={FaNetworkWired}
|
||||
value={`${t("common.byterate", { value: data.network.tx_sec })} ↑`}
|
||||
label={`${t("common.byterate", { value: data.network.rx_sec })} ↓`}
|
||||
expandedValue={`${t("common.bytes", { value: data.network.tx_bytes })} ↑`}
|
||||
expandedLabel={`${t("common.bytes", { value: data.network.rx_bytes })} ↓`}
|
||||
expanded={options.expanded}
|
||||
wide
|
||||
percentage={(100 * data.network.rx_sec) / (data.network.rx_sec + data.network.tx_sec)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Cpu from "./cpu";
|
||||
import Memory from "./memory";
|
||||
import CpuTemp from "./cputemp";
|
||||
import Uptime from "./uptime";
|
||||
import Network from "./network";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
const { expanded, units, diskUnits, tempmin, tempmax } = options;
|
||||
@@ -23,6 +24,7 @@ export default function Resources({ options }) {
|
||||
<Disk key={disk} options={{ disk }} expanded={expanded} diskUnits={diskUnits} refresh={refresh} />
|
||||
))
|
||||
: options.disk && <Disk options={options} expanded={expanded} diskUnits={diskUnits} refresh={refresh} />}
|
||||
{options.network && <Network options={options} refresh={refresh} />}
|
||||
{options.cputemp && (
|
||||
<CpuTemp expanded={expanded} units={units} refresh={refresh} tempmin={tempmin} tempmax={tempmax} />
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect, Fragment } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle, SiBaidu, SiBrave } from "react-icons/si";
|
||||
import { SiDuckduckgo, SiGoogle, SiBaidu, SiBrave } from "react-icons/si";
|
||||
import { BiLogoBing } from "react-icons/bi";
|
||||
import { Listbox, Transition, Combobox } from "@headlessui/react";
|
||||
import classNames from "classnames";
|
||||
|
||||
@@ -25,7 +26,7 @@ export const searchProviders = {
|
||||
name: "Bing",
|
||||
url: "https://www.bing.com/search?q=",
|
||||
suggestionUrl: "https://api.bing.com/osjson.aspx?query=",
|
||||
icon: SiMicrosoftbing,
|
||||
icon: BiLogoBing,
|
||||
},
|
||||
baidu: {
|
||||
name: "Baidu",
|
||||
@@ -94,6 +95,7 @@ export default function Search({ options }) {
|
||||
if (
|
||||
options.showSearchSuggestions &&
|
||||
(selectedProvider.suggestionUrl || options.suggestionUrl) && // custom providers pass url via options
|
||||
query.trim().length > 0 &&
|
||||
query.trim() !== searchSuggestions[0]
|
||||
) {
|
||||
fetch(`/api/search/searchSuggestion?query=${encodeURIComponent(query)}&providerName=${selectedProvider.name}`, {
|
||||
|
||||
@@ -51,7 +51,9 @@ export default function Widget({ options }) {
|
||||
key={stock.ticker}
|
||||
className="rounded h-full text-xs px-1 w-[4.75rem] flex flex-col items-center justify-center"
|
||||
>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{stock.ticker}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{stock.ticker.split(":").pop()}
|
||||
</span>
|
||||
{!viewingPercentChange ? (
|
||||
<span
|
||||
className={
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import classNames from "classnames";
|
||||
import { useContext } from "react";
|
||||
|
||||
import WidgetIcon from "./widget_icon";
|
||||
import PrimaryText from "./primary_text";
|
||||
import SecondaryText from "./secondary_text";
|
||||
import Raw from "./raw";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
|
||||
export function getAllClasses(options, additionalClassNames = "") {
|
||||
if (options?.style?.header === "boxedWidgets") {
|
||||
if (options?.style?.cardBlur !== undefined) {
|
||||
@@ -56,7 +59,17 @@ export function getBottomBlock(children) {
|
||||
}
|
||||
|
||||
export default function Container({ children = [], options, additionalClassNames = "" }) {
|
||||
return (
|
||||
const { settings } = useContext(SettingsContext);
|
||||
return options?.href ? (
|
||||
<a
|
||||
href={options.href}
|
||||
target={options.target ?? settings.target ?? "_blank"}
|
||||
className={getAllClasses(options, `${additionalClassNames} widget-container`)}
|
||||
>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</a>
|
||||
) : (
|
||||
<div className={getAllClasses(options, `${additionalClassNames} widget-container`)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function Resource({
|
||||
percentage,
|
||||
expanded = false,
|
||||
additionalClassNames = "",
|
||||
wide = false,
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
@@ -18,7 +19,11 @@ export default function Resource({
|
||||
className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${additionalClassNames}`}
|
||||
>
|
||||
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5 resource-icon" />
|
||||
<div className={`flex flex-col ml-3 text-left min-w-[85px] ${expanded ? " expanded" : ""}`}>
|
||||
<div
|
||||
className={`flex flex-col ml-3 text-left ${expanded ? " expanded" : ""} ${
|
||||
wide ? " min-w-[120px]" : "min-w-[85px]"
|
||||
}`}
|
||||
>
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
|
||||
<div className="pl-0.5">{value}</div>
|
||||
<div className="pr-1">{label}</div>
|
||||
|
||||
@@ -4,12 +4,10 @@ export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<meta
|
||||
name="description"
|
||||
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
||||
/>
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
|
||||
<link rel="preload" href="/api/config/custom.css" as="style" />
|
||||
<link rel="stylesheet" href="/api/config/custom.css" /> {/* eslint-disable-line @next/next/no-css-tags */}
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
||||
@@ -6,10 +6,10 @@ import createLogger from "utils/logger";
|
||||
const logger = createLogger("ping");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceItem = await getServiceItem(group, service);
|
||||
const { groupName, serviceName } = req.query;
|
||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||
if (!serviceItem) {
|
||||
logger.debug(`No service item found for group ${group} named ${service}`);
|
||||
logger.debug(`No service item found for group ${groupName} named ${serviceName}`);
|
||||
return res.status(400).send({
|
||||
error: "Unable to find service, see log for details.",
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ const logger = createLogger("servicesProxy");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
try {
|
||||
const { service, group } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const { service, group, index } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
let type = serviceWidget?.type;
|
||||
|
||||
// exceptions
|
||||
@@ -41,7 +41,7 @@ export default async function handler(req, res) {
|
||||
const endpoint = mapping?.endpoint;
|
||||
const endpointProxy = mapping?.proxyHandler || serviceProxyHandler;
|
||||
|
||||
if (mapping.method && mapping.method !== req.method) {
|
||||
if (mapping?.method && mapping.method !== req.method) {
|
||||
logger.debug("Unsupported method: %s", req.method);
|
||||
return res.status(403).json({ error: "Unsupported method" });
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { httpProxy } from "utils/proxy/http";
|
||||
const logger = createLogger("siteMonitor");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceItem = await getServiceItem(group, service);
|
||||
const { groupName, serviceName } = req.query;
|
||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||
if (!serviceItem) {
|
||||
logger.debug(`No service item found for group ${group} named ${service}`);
|
||||
logger.debug(`No service item found for group ${groupName} named ${serviceName}`);
|
||||
return res.status(400).send({
|
||||
error: "Unable to find service, see log for details.",
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ const logger = createLogger("resources");
|
||||
const si = require("systeminformation");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type, target } = req.query;
|
||||
const { type, target, interfaceName = "default" } = req.query;
|
||||
|
||||
if (type === "cpu") {
|
||||
const load = await si.currentLoad();
|
||||
@@ -57,6 +57,32 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "network") {
|
||||
let networkData = await si.networkStats();
|
||||
let interfaceDefault;
|
||||
logger.debug("networkData:", JSON.stringify(networkData));
|
||||
if (interfaceName && interfaceName !== "default") {
|
||||
networkData = networkData.filter((network) => network.iface === interfaceName).at(0);
|
||||
if (!networkData) {
|
||||
return res.status(404).json({
|
||||
error: "Interface not found",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
interfaceDefault = await si.networkInterfaceDefault();
|
||||
networkData = networkData.filter((network) => network.iface === interfaceDefault).at(0);
|
||||
if (!networkData) {
|
||||
return res.status(404).json({
|
||||
error: "Default interface not found",
|
||||
});
|
||||
}
|
||||
}
|
||||
return res.status(200).json({
|
||||
network: networkData,
|
||||
interface: interfaceName !== "default" ? interfaceName : interfaceDefault,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
error: "invalid type",
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import cachedFetch from "utils/proxy/cached-fetch";
|
||||
import { getSettings } from "utils/config/config";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("stocks");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { watchlist, provider, cache } = req.query;
|
||||
|
||||
logger.debug("Stocks API request: %o", { watchlist, provider, cache });
|
||||
|
||||
if (!watchlist) {
|
||||
return res.status(400).json({ error: "Missing watchlist" });
|
||||
}
|
||||
@@ -56,6 +61,7 @@ export default async function handler(req, res) {
|
||||
// Finnhub free accounts allow up to 60 calls/minute
|
||||
// https://finnhub.io/pricing
|
||||
const { c, dp } = await cachedFetch(apiUrl, cache || 1);
|
||||
logger.debug("Finnhub API response for %s: %o", ticker, { c, dp });
|
||||
|
||||
// API sometimes returns 200, but values returned are `null`
|
||||
if (c === null || dp === null) {
|
||||
|
||||
+29
-11
@@ -166,6 +166,18 @@ const headerStyles = {
|
||||
boxedWidgets: "m-5 mb-0 sm:m-9 sm:mb-0 sm:mt-1",
|
||||
};
|
||||
|
||||
function getAllServices(services) {
|
||||
function getServices(group) {
|
||||
let nestedServices = [...group.services];
|
||||
if (group.groups.length > 0) {
|
||||
nestedServices = [...nestedServices, ...group.groups.map(getServices).flat()];
|
||||
}
|
||||
return nestedServices;
|
||||
}
|
||||
|
||||
return [...services.map(getServices).flat()];
|
||||
}
|
||||
|
||||
function Home({ initialSettings }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
@@ -182,10 +194,9 @@ function Home({ initialSettings }) {
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
|
||||
const servicesAndBookmarks = [
|
||||
...services.map((sg) => sg.services).flat(),
|
||||
...bookmarks.map((bg) => bg.bookmarks).flat(),
|
||||
].filter((i) => i?.href);
|
||||
const servicesAndBookmarks = [...bookmarks.map((bg) => bg.bookmarks).flat(), ...getAllServices(services)].filter(
|
||||
(i) => i?.href,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.language) {
|
||||
@@ -291,8 +302,7 @@ function Home({ initialSettings }) {
|
||||
group.services ? (
|
||||
<ServicesGroup
|
||||
key={group.name}
|
||||
group={group.name}
|
||||
services={group}
|
||||
group={group}
|
||||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
@@ -316,8 +326,7 @@ function Home({ initialSettings }) {
|
||||
{serviceGroups.map((group) => (
|
||||
<ServicesGroup
|
||||
key={group.name}
|
||||
group={group.name}
|
||||
services={group}
|
||||
group={group}
|
||||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
@@ -335,6 +344,7 @@ function Home({ initialSettings }) {
|
||||
layout={settings.layout?.[group.name]}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
|
||||
bookmarksStyle={settings.bookmarksStyle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -352,13 +362,21 @@ function Home({ initialSettings }) {
|
||||
settings.useEqualHeights,
|
||||
settings.cardBlur,
|
||||
settings.groupsInitiallyCollapsed,
|
||||
settings.bookmarksStyle,
|
||||
initialSettings.layout,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
<title>{initialSettings.title || "Homepage"}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
initialSettings.description ||
|
||||
"A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
||||
}
|
||||
/>
|
||||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon ? (
|
||||
<>
|
||||
@@ -376,8 +394,6 @@ function Home({ initialSettings }) {
|
||||
)}
|
||||
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||
<link rel="preload" href="/api/config/custom.css" as="style" />
|
||||
<link rel="stylesheet" href="/api/config/custom.css" /> {/* eslint-disable-line @next/next/no-css-tags */}
|
||||
</Head>
|
||||
|
||||
<Script src="/api/config/custom.js" />
|
||||
@@ -454,6 +470,7 @@ function Home({ initialSettings }) {
|
||||
}
|
||||
|
||||
export default function Wrapper({ initialSettings, fallback }) {
|
||||
const { themeContext } = useContext(ThemeContext);
|
||||
const wrappedStyle = {};
|
||||
let backgroundBlur = false;
|
||||
let backgroundSaturate = false;
|
||||
@@ -486,6 +503,7 @@ export default function Wrapper({ initialSettings, fallback }) {
|
||||
"relative",
|
||||
initialSettings.theme && initialSettings.theme,
|
||||
initialSettings.color && `theme-${initialSettings.color}`,
|
||||
themeContext === "dark" ? "scheme-dark" : "scheme-light",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/services
|
||||
# https://gethomepage.dev/configs/services/
|
||||
|
||||
- My First Group:
|
||||
- My First Service:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/settings
|
||||
# https://gethomepage.dev/configs/settings/
|
||||
|
||||
providers:
|
||||
openweathermap: openweathermapapikey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/service-widgets
|
||||
# https://gethomepage.dev/configs/info-widgets/
|
||||
|
||||
- resources:
|
||||
cpu: true
|
||||
|
||||
@@ -16,6 +16,7 @@ body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
#page_wrapper {
|
||||
@@ -62,3 +63,7 @@ dialog ::-webkit-scrollbar {
|
||||
::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chart + .chart {
|
||||
margin-top: 2em;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
servicesFromDocker,
|
||||
cleanServiceGroups,
|
||||
servicesFromKubernetes,
|
||||
findGroupByName,
|
||||
} from "utils/config/service-helpers";
|
||||
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
|
||||
|
||||
@@ -84,6 +85,17 @@ export async function widgetsResponse() {
|
||||
return configuredWidgets;
|
||||
}
|
||||
|
||||
function mergeSubgroups(configuredGroups, mergedGroup) {
|
||||
configuredGroups.forEach((group) => {
|
||||
if (group.name === mergedGroup.name) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
group.services = mergedGroup.services;
|
||||
} else if (group.groups) {
|
||||
mergeSubgroups(group.groups, mergedGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function servicesResponse() {
|
||||
let discoveredDockerServices;
|
||||
let discoveredKubernetesServices;
|
||||
@@ -140,25 +152,29 @@ export async function servicesResponse() {
|
||||
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
|
||||
|
||||
mergedGroupsNames.forEach((groupName) => {
|
||||
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || {
|
||||
const discoveredDockerGroup = findGroupByName(discoveredDockerServices, groupName) || {
|
||||
services: [],
|
||||
};
|
||||
const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || {
|
||||
const discoveredKubernetesGroup = findGroupByName(discoveredKubernetesServices, groupName) || {
|
||||
services: [],
|
||||
};
|
||||
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const configuredGroup = findGroupByName(configuredServices, groupName) || { services: [], groups: [] };
|
||||
|
||||
const mergedGroup = {
|
||||
name: groupName,
|
||||
services: [...discoveredDockerGroup.services, ...discoveredKubernetesGroup.services, ...configuredGroup.services]
|
||||
.filter((service) => service)
|
||||
.sort(compareServices),
|
||||
groups: [...configuredGroup.groups],
|
||||
};
|
||||
|
||||
if (definedLayouts) {
|
||||
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);
|
||||
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
|
||||
else unsortedGroups.push(mergedGroup);
|
||||
else if (configuredGroup.parent) {
|
||||
// this is a nested group, so find the parent group and merge the services
|
||||
mergeSubgroups(configuredServices, mergedGroup);
|
||||
} else unsortedGroups.push(mergedGroup);
|
||||
} else {
|
||||
unsortedGroups.push(mergedGroup);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getURLSearchParams(widget, endpoint) {
|
||||
const params = new URLSearchParams({
|
||||
group: widget.service_group,
|
||||
service: widget.service_name,
|
||||
index: widget.index,
|
||||
});
|
||||
if (endpoint) {
|
||||
params.append("endpoint", endpoint);
|
||||
|
||||
@@ -9,10 +9,10 @@ import widgets from "widgets/widgets";
|
||||
const logger = createLogger("credentialedProxyHandler");
|
||||
|
||||
export default async function credentialedProxyHandler(req, res, map) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
@@ -89,11 +89,15 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
} else if (widget.type === "myspeed") {
|
||||
headers.Password = `${widget.password}`;
|
||||
} else if (widget.type === "esphome") {
|
||||
if (widget.key) {
|
||||
if (widget.username && widget.password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||
} else if (widget.key) {
|
||||
headers.Cookie = `authenticated=${widget.key}`;
|
||||
}
|
||||
} else if (widget.type === "wgeasy") {
|
||||
headers.Authorization = widget.password;
|
||||
} else if (widget.type === "gitlab") {
|
||||
headers["PRIVATE-TOKEN"] = widget.key;
|
||||
} else {
|
||||
headers["X-API-Key"] = `${widget.key}`;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import widgets from "widgets/widgets";
|
||||
const logger = createLogger("genericProxyHandler");
|
||||
|
||||
export default async function genericProxyHandler(req, res, map) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
@@ -79,7 +79,7 @@ export default async function genericProxyHandler(req, res, map) {
|
||||
error: {
|
||||
message: "HTTP Error",
|
||||
url: sanitizeErrorURL(url),
|
||||
resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
|
||||
data: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ export async function sendJsonRpcRequest(url, method, params, widget) {
|
||||
}
|
||||
|
||||
export default async function jsonrpcProxyHandler(req, res) {
|
||||
const { group, service, endpoint: method } = req.query;
|
||||
const { group, service, endpoint: method, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
|
||||
const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method);
|
||||
|
||||
@@ -131,13 +131,13 @@ function toError(url, synologyError) {
|
||||
}
|
||||
|
||||
export default async function synologyProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
const widget = widgets?.[serviceWidget.type];
|
||||
const mapping = widget?.mappings?.[endpoint];
|
||||
if (!widget.api || !mapping) {
|
||||
|
||||
@@ -23,14 +23,14 @@ async function retrieveFromAPI(url, key) {
|
||||
}
|
||||
|
||||
export default async function audiobookshelfProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -20,8 +20,20 @@ export default function Component({ service }) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (systemsError) {
|
||||
return <Container service={service} error={systemsError} />;
|
||||
let system = null;
|
||||
let finalError = systemsError;
|
||||
|
||||
if (systems && !systems.items) {
|
||||
finalError = { message: "No items returned from beszel API" };
|
||||
} else if (systems && systems.items && systemId) {
|
||||
system = systems.items.find((item) => item.id === systemId || item.name === systemId);
|
||||
if (!system) {
|
||||
finalError = { message: `System with id ${systemId} not found` };
|
||||
}
|
||||
}
|
||||
|
||||
if (finalError) {
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
if (!systems) {
|
||||
@@ -33,9 +45,7 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (systemId) {
|
||||
const system = systems.items.find((item) => item.id === systemId);
|
||||
|
||||
if (system) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="beszel.name" value={system.name} />
|
||||
|
||||
@@ -34,10 +34,10 @@ async function login(loginUrl, username, password, service) {
|
||||
}
|
||||
|
||||
export default async function beszelProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
@@ -45,7 +45,12 @@ export default async function beszelProxyHandler(req, res) {
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
const loginUrl = formatApiCall(widgets[widget.type].api, { endpoint: "admins/auth-with-password", ...widget });
|
||||
let authEndpointVersion = "authv1";
|
||||
if (widget.version === 2) authEndpointVersion = "authv2";
|
||||
const loginUrl = formatApiCall(widgets[widget.type].api, {
|
||||
endpoint: widgets[widget.type].mappings[authEndpointVersion].endpoint,
|
||||
...widget,
|
||||
});
|
||||
|
||||
let status;
|
||||
let data;
|
||||
@@ -54,7 +59,7 @@ export default async function beszelProxyHandler(req, res) {
|
||||
if (!token) {
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||
logger.debug(`HTTP ${status} logging into Beszel: ${JSON.stringify(token)}`);
|
||||
return res.status(status).send(token);
|
||||
}
|
||||
}
|
||||
@@ -67,13 +72,13 @@ export default async function beszelProxyHandler(req, res) {
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 403) {
|
||||
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
||||
if ([400, 403].includes(status)) {
|
||||
logger.debug(`HTTP ${status} retrieving data from Beszel, logging in and trying again.`);
|
||||
cache.del(`${tokenCacheKey}.${service}`);
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
|
||||
logger.debug(`HTTP ${status} logging into Beszel: ${JSON.stringify(data)}`);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ const widget = {
|
||||
proxyHandler: beszelProxyHandler,
|
||||
|
||||
mappings: {
|
||||
authv1: {
|
||||
endpoint: "admins/auth-with-password",
|
||||
},
|
||||
authv2: {
|
||||
endpoint: "collections/_superusers/auth-with-password",
|
||||
},
|
||||
systems: {
|
||||
endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ const cellStyle = "relative w-10 flex items-center justify-center flex-col";
|
||||
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
|
||||
|
||||
export function Day({ weekNumber, weekday, events, colorVariants, showDate, setShowDate, currentDate }) {
|
||||
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
|
||||
const cellDate = showDate.set({ weekday, weekNumber, weekYear: showDate.year }).startOf("day");
|
||||
const filteredEvents = events?.filter((event) => compareDateTimezone(cellDate, event));
|
||||
|
||||
const dayStyles = (displayDate) => {
|
||||
@@ -35,7 +35,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
|
||||
// today style
|
||||
style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md ";
|
||||
} else {
|
||||
style += "hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer ";
|
||||
style += "hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
|
||||
}
|
||||
|
||||
return style;
|
||||
|
||||
@@ -5,10 +5,10 @@ import createLogger from "utils/logger";
|
||||
const logger = createLogger("calendarProxyHandler");
|
||||
|
||||
export default async function calendarProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
const integration = widget.integrations?.find((i) => i.name === endpoint);
|
||||
|
||||
if (integration) {
|
||||
|
||||
@@ -38,6 +38,7 @@ const components = {
|
||||
gatus: dynamic(() => import("./gatus/component")),
|
||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||
gitea: dynamic(() => import("./gitea/component")),
|
||||
gitlab: dynamic(() => import("./gitlab/component")),
|
||||
glances: dynamic(() => import("./glances/component")),
|
||||
gluetun: dynamic(() => import("./gluetun/component")),
|
||||
gotify: dynamic(() => import("./gotify/component")),
|
||||
@@ -111,6 +112,7 @@ const components = {
|
||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
spoolman: dynamic(() => import("./spoolman/component")),
|
||||
stash: dynamic(() => import("./stash/component")),
|
||||
stocks: dynamic(() => import("./stocks/component")),
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
|
||||
@@ -35,14 +35,14 @@ async function login(widget, service) {
|
||||
}
|
||||
|
||||
export default async function crowdsecProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.error("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget || !widgets[widget.type].api) {
|
||||
logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid widget configuration" });
|
||||
|
||||
@@ -78,6 +78,9 @@ function formatValue(t, mapping, rawValue) {
|
||||
case "percent":
|
||||
value = t("common.percent", { value });
|
||||
break;
|
||||
case "duration":
|
||||
value = t("common.duration", { value });
|
||||
break;
|
||||
case "bytes":
|
||||
value = t("common.bytes", { value });
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
@@ -32,21 +34,38 @@ export default function Component({ service }) {
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
const leechTorrents = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
const torrent = torrents[keys[i]];
|
||||
rateDl += torrent.download_payload_rate;
|
||||
rateUl += torrent.upload_payload_rate;
|
||||
completed += torrent.total_remaining === 0 ? 1 : 0;
|
||||
if (torrent.state === "Downloading") {
|
||||
leechTorrents.push(torrent);
|
||||
}
|
||||
}
|
||||
|
||||
const leech = keys.length - completed || 0;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="deluge.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="deluge.download" value={t("common.byterate", { value: rateDl })} />
|
||||
<Block label="deluge.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="deluge.upload" value={t("common.byterate", { value: rateUl })} />
|
||||
</Container>
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="deluge.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="deluge.download" value={t("common.byterate", { value: rateDl })} />
|
||||
<Block label="deluge.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="deluge.upload" value={t("common.byterate", { value: rateUl })} />
|
||||
</Container>
|
||||
{widget?.enableLeechProgress &&
|
||||
leechTorrents.map((queueEntry) => (
|
||||
<QueueEntry
|
||||
progress={queueEntry.progress}
|
||||
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
||||
title={queueEntry.name}
|
||||
activity={queueEntry.state}
|
||||
key={`${queueEntry.name}-${queueEntry.total_remaining}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const dataParams = [
|
||||
"download_payload_rate",
|
||||
"upload_payload_rate",
|
||||
"total_remaining",
|
||||
"eta",
|
||||
],
|
||||
{},
|
||||
];
|
||||
@@ -40,14 +41,14 @@ function login(url, password) {
|
||||
}
|
||||
|
||||
export default async function delugeProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -4,6 +4,10 @@ import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
function toKilowatts(t, value) {
|
||||
return value > 0 ? t("common.number", { value: value / 1000, maximumFractionDigits: 1 }) : 0;
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -25,23 +29,17 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
// broken by evcc v0.133.0 https://github.com/evcc-io/evcc/commit/9dcb1fa0a7c08dd926b79309aa1f676a5fc6c8aa
|
||||
const gridPower = stateData.result.gridPower ?? stateData.result.grid?.power ?? 0;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="evcc.pv_power"
|
||||
value={`${t("common.number", { value: stateData.result.pvPower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block
|
||||
label="evcc.grid_power"
|
||||
value={`${t("common.number", { value: stateData.result.gridPower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block
|
||||
label="evcc.home_power"
|
||||
value={`${t("common.number", { value: stateData.result.homePower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block label="evcc.pv_power" value={`${toKilowatts(t, stateData.result.pvPower)} ${t("evcc.kilowatt")}`} />
|
||||
<Block label="evcc.grid_power" value={`${toKilowatts(t, gridPower)} ${t("evcc.kilowatt")}`} />
|
||||
<Block label="evcc.home_power" value={`${toKilowatts(t, stateData.result.homePower)} ${t("evcc.kilowatt")}`} />
|
||||
<Block
|
||||
label="evcc.charge_power"
|
||||
value={`${t("common.number", { value: stateData.result.loadpoints[0].chargePower })} ${t("evcc.watt_hour")}`}
|
||||
value={`${toKilowatts(t, stateData.result.loadpoints[0].chargePower)} ${t("evcc.kilowatt")}`}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -28,14 +28,14 @@ async function login(widget) {
|
||||
}
|
||||
|
||||
export default async function floodProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -74,14 +74,14 @@ async function apiCall(widget, endpoint, service) {
|
||||
}
|
||||
|
||||
export default async function freshrssProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
|
||||
@@ -46,8 +46,8 @@ async function requestEndpoint(apiBaseUrl, service, action) {
|
||||
}
|
||||
|
||||
export default async function fritzboxProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const { group, service, index } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!serviceWidget) {
|
||||
res.status(500).json({ error: { message: "Service widget not found" } });
|
||||
|
||||
@@ -7,8 +7,8 @@ const proxyName = "gamedigProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function gamedigProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const { group, service, index } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
const url = new URL(serviceWidget.url);
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: gitlabCounts, error: gitlabCountsError } = useWidgetAPI(widget, "counts");
|
||||
|
||||
if (gitlabCountsError) {
|
||||
return <Container service={service} error={gitlabCountsError} />;
|
||||
}
|
||||
|
||||
if (!gitlabCounts) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gitlab.groups" />
|
||||
<Block label="gitlab.issues" />
|
||||
<Block label="gitlab.merges" />
|
||||
<Block label="gitlab.projects" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gitlab.groups" value={t("common.number", { value: gitlabCounts.groups_count })} />
|
||||
<Block label="gitlab.issues" value={t("common.number", { value: gitlabCounts.issues_count })} />
|
||||
<Block label="gitlab.merges" value={t("common.number", { value: gitlabCounts.merge_requests_count })} />
|
||||
<Block label="gitlab.projects" value={t("common.number", { value: gitlabCounts.projects_count })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v4/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
mappings: {
|
||||
counts: {
|
||||
endpoint: "users/{user_id}/associations_count",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -7,6 +7,7 @@ import Disk from "./metrics/disk";
|
||||
import GPU from "./metrics/gpu";
|
||||
import Info from "./metrics/info";
|
||||
import Fs from "./metrics/fs";
|
||||
import Containers from "./metrics/containers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
@@ -23,6 +24,10 @@ export default function Component({ service }) {
|
||||
return <Process service={service} />;
|
||||
}
|
||||
|
||||
if (widget.metric === "containers") {
|
||||
return <Containers service={service} />;
|
||||
}
|
||||
|
||||
if (widget.metric === "cpu") {
|
||||
return <Cpu service={service} />;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ class Chart extends PureComponent {
|
||||
const { dataPoints, formatter, label } = this.props;
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
|
||||
<div className="absolute -top-10 -left-2 h-[calc(100%+3em)] w-[calc(100%+1em)] z-0">
|
||||
<div className="overflow-clip z-10 w-full h-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={dataPoints}>
|
||||
|
||||
@@ -8,7 +8,7 @@ class ChartDual extends PureComponent {
|
||||
const { dataPoints, formatter, stack, label, stackOffset } = this.props;
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
|
||||
<div className="absolute -top-10 -left-2 h-[calc(100%+3em)] w-[calc(100%+1em)] z-0">
|
||||
<div className="overflow-clip z-10 w-full h-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={dataPoints} stackOffset={stackOffset ?? "none"}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Error from "./error";
|
||||
|
||||
@@ -17,9 +18,9 @@ export default function Container({ children, widget, error = null, chart = true
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classNames("service-container", chart ? "chart relative h-[68px]" : "")}>
|
||||
{children}
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 overflow-clip pointer-events-none ${className}`} />
|
||||
<div className={`absolute -top-10 right-0 bottom-0 left-0 overflow-clip pointer-events-none ${className}`} />
|
||||
{chart && <div className="h-[68px] overflow-clip" />}
|
||||
{!chart && <div className="h-[16px] overflow-clip" />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "../components/container";
|
||||
import Block from "../components/block";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
const statusMap = {
|
||||
running: <ResolvedIcon icon="mdi-circle" width={32} height={32} />,
|
||||
paused: <ResolvedIcon icon="mdi-circle-outline" width={32} height={32} />,
|
||||
};
|
||||
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
|
||||
|
||||
const idKey = version === 3 ? "Id" : "id";
|
||||
const statusKey = version === 3 ? "Status" : "status";
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, `${version}/containers`, {
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} widget={widget} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
data.splice(chart ? 5 : 1);
|
||||
let headerYPosition = "top-4";
|
||||
let listYPosition = "bottom-4";
|
||||
if (chart) {
|
||||
headerYPosition = "-top-6";
|
||||
listYPosition = "-top-3";
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position={`${headerYPosition} right-3 left-3`}>
|
||||
<div className="flex items-center text-xs">
|
||||
<div className="grow" />
|
||||
<div className="w-14 text-right italic">{t("resources.cpu")}</div>
|
||||
<div className="w-14 text-right">{t("resources.mem")}</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block position={`${listYPosition} right-3 left-3`}>
|
||||
<div className="pointer-events-none text-theme-900 dark:text-theme-200">
|
||||
{data.map((item) => (
|
||||
<div key={item[idKey]} className="text-[0.75rem] h-[0.8rem]">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 mr-1.5 opacity-50">{statusMap[item[statusKey]]}</div>
|
||||
<div className="opacity-75 grow truncate">{item.name}</div>
|
||||
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
|
||||
<div className="opacity-25 w-14 text-right">
|
||||
{t("common.bytes", {
|
||||
value: item.memory.usage,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -43,10 +43,10 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{chart && (
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0">
|
||||
<div className="absolute -top-2 -left-2 -right-2 -bottom-2">
|
||||
<div
|
||||
style={{
|
||||
height: `${Math.max(20, fsData.size / fsData.free)}%`,
|
||||
height: `${Math.max(20, (140 * (fsData.size - fsData.free)) / fsData.size)}px`,
|
||||
}}
|
||||
className="absolute bottom-0 border-t border-t-theme-500 bg-gradient-to-b from-theme-500/40 to-theme-500/10 w-full"
|
||||
/>
|
||||
|
||||
@@ -107,8 +107,12 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart} className="bg-gradient-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10">
|
||||
<Block position="top-3 right-3">
|
||||
<Container chart={chart}>
|
||||
{chart && (
|
||||
<div className="bg-gradient-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10 absolute -top-10 -left-2 -right-2 -bottom-2 h-[calc(100%+3em)] w-[calc(100%+1em)]" />
|
||||
)}
|
||||
|
||||
<Block position="-top-6 right-2">
|
||||
{quicklookData && quicklookData.cpu_name && chart && (
|
||||
<div className="text-[0.6rem] opacity-50">{quicklookData.cpu_name}</div>
|
||||
)}
|
||||
@@ -124,7 +128,7 @@ export default function Component({ service }) {
|
||||
</Block>
|
||||
|
||||
{chart && (
|
||||
<Block position="bottom-3 left-3">
|
||||
<Block position="bottom-3 left-2">
|
||||
{systemData && systemData.linux_distro && <div className="text-xs opacity-50">{systemData.linux_distro}</div>}
|
||||
{systemData && systemData.os_version && <div className="text-xs opacity-50">{systemData.os_version}</div>}
|
||||
{systemData && systemData.hostname && <div className="text-xs opacity-75">{systemData.hostname}</div>}
|
||||
@@ -137,7 +141,7 @@ export default function Component({ service }) {
|
||||
</Block>
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 right-3 w-[4rem]">
|
||||
<Block position="bottom-3 right-2 w-[4rem]">
|
||||
{chart && <CPU quicklookData={quicklookData} className="opacity-50" />}
|
||||
|
||||
{chart && <Mem quicklookData={quicklookData} className="opacity-50" />}
|
||||
|
||||
@@ -42,10 +42,16 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
data.splice(chart ? 5 : 1);
|
||||
let headerYPosition = "top-4";
|
||||
let listYPosition = "bottom-4";
|
||||
if (chart) {
|
||||
headerYPosition = "-top-6";
|
||||
listYPosition = "-top-3";
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="top-4 right-3 left-3">
|
||||
<Block position={`${headerYPosition} right-3 left-3`}>
|
||||
<div className="flex items-center text-xs">
|
||||
<div className="grow" />
|
||||
<div className="w-14 text-right italic">{t("resources.cpu")}</div>
|
||||
@@ -53,7 +59,7 @@ export default function Component({ service }) {
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block position="bottom-4 right-3 left-3">
|
||||
<Block position={`${listYPosition} right-3 left-3`}>
|
||||
<div className="pointer-events-none text-theme-900 dark:text-theme-200">
|
||||
{data.map((item) => (
|
||||
<div key={item.pid} className="text-[0.75rem] h-[0.8rem]">
|
||||
@@ -63,7 +69,7 @@ export default function Component({ service }) {
|
||||
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
|
||||
<div className="opacity-25 w-14 text-right">
|
||||
{t("common.bytes", {
|
||||
value: item.memory_info[memoryInfoKey],
|
||||
value: item.memory_info[memoryInfoKey] ?? item.memory_info.wset,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
allowedEndpoints: /\d\/quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors/,
|
||||
allowedEndpoints: /\d\/quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors|containers/,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -7,7 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
ip: {
|
||||
endpoint: "publicip/ip",
|
||||
validate: ["public_ip", "region", "country"],
|
||||
validate: ["public_ip", "country"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -62,14 +62,14 @@ async function getQuery(query, { url, key }) {
|
||||
}
|
||||
|
||||
export default async function homeassistantProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
|
||||
@@ -68,14 +68,14 @@ async function apiCall(widget, endpoint, service) {
|
||||
}
|
||||
|
||||
export default async function homeboxProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
|
||||
@@ -71,14 +71,14 @@ async function apiCall(widget, endpoint, service) {
|
||||
}
|
||||
|
||||
export default async function homebridgeProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -25,14 +25,14 @@ async function fetchJackettCookie(widget, loginURL) {
|
||||
}
|
||||
|
||||
export default async function jackettProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.error("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget || !widgets[widget.type].api) {
|
||||
logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid widget configuration" });
|
||||
|
||||
@@ -12,12 +12,12 @@ const proxyName = "jdownloaderProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return null;
|
||||
|
||||
@@ -70,14 +70,14 @@ async function apiCall(widget, endpoint, service) {
|
||||
}
|
||||
|
||||
export default async function KavitaProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
|
||||
@@ -7,8 +7,8 @@ const proxyName = "minecraftProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function minecraftProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const { group, service, index } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
const url = new URL(serviceWidget.url);
|
||||
try {
|
||||
const pingResponse = await pingWithPromise(url.hostname, url.port || 25565);
|
||||
|
||||
@@ -36,10 +36,10 @@ async function login(loginUrl, username, password, service) {
|
||||
}
|
||||
|
||||
export default async function npmProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
|
||||
@@ -33,10 +33,10 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||
}
|
||||
|
||||
export default async function omadaProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (widget) {
|
||||
const { url } = widget;
|
||||
|
||||
@@ -12,14 +12,14 @@ const BG_POLL_PERIOD = 500;
|
||||
const logger = createLogger(PROXY_NAME);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -17,14 +17,14 @@ const PARAMS = {
|
||||
};
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -6,14 +6,14 @@ import createLogger from "utils/logger";
|
||||
const logger = createLogger("photoprismProxyHandler");
|
||||
|
||||
export default async function photoprismProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -33,7 +33,7 @@ async function login(widget, service) {
|
||||
}
|
||||
|
||||
export default async function piholeProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
let endpoint = "stats/summary";
|
||||
|
||||
if (!group || !service) {
|
||||
@@ -41,7 +41,7 @@ export default async function piholeProxyHandler(req, res) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid widget configuration" });
|
||||
|
||||
@@ -16,14 +16,14 @@ const tvCacheKey = `${proxyName}__tv`;
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -67,11 +67,11 @@ async function login(loginUrl, service, username, password = "") {
|
||||
}
|
||||
|
||||
export default async function pyloadProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
try {
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (widget) {
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import QueueEntry from "../../components/widgets/queue/queueEntry";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
|
||||
@@ -29,6 +30,7 @@ export default function Component({ service }) {
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
const leechTorrents = [];
|
||||
|
||||
for (let i = 0; i < torrentData.length; i += 1) {
|
||||
const torrent = torrentData[i];
|
||||
@@ -37,16 +39,31 @@ export default function Component({ service }) {
|
||||
if (torrent.progress === 1) {
|
||||
completed += 1;
|
||||
}
|
||||
if (torrent.state.includes("DL") || torrent.state === "downloading") {
|
||||
leechTorrents.push(torrent);
|
||||
}
|
||||
}
|
||||
|
||||
const leech = torrentData.length - completed;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
|
||||
<Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
|
||||
</Container>
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
|
||||
<Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
|
||||
</Container>
|
||||
{widget?.enableLeechProgress &&
|
||||
leechTorrents.map((queueEntry) => (
|
||||
<QueueEntry
|
||||
progress={queueEntry.progress * 100}
|
||||
timeLeft={t("common.duration", { value: queueEntry.eta })}
|
||||
title={queueEntry.name}
|
||||
activity={queueEntry.state}
|
||||
key={`${queueEntry.name}-${queueEntry.amount_left}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,14 +21,14 @@ async function login(widget) {
|
||||
}
|
||||
|
||||
export default async function qbittorrentProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -77,14 +77,14 @@ async function apiCall(widget, endpoint, service) {
|
||||
}
|
||||
|
||||
export default async function qnapProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
|
||||
@@ -45,10 +45,10 @@ const getTorrentInfo = (data) => ({
|
||||
});
|
||||
|
||||
export default async function rutorrentProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (widget) {
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { data: spoolData, error: spoolError } = useWidgetAPI(widget, "spools");
|
||||
|
||||
if (spoolError) {
|
||||
return <Container service={service} error={spoolError} />;
|
||||
}
|
||||
|
||||
if (!spoolData) {
|
||||
const nBlocksGuess = widget.spoolIds?.length ?? 4;
|
||||
return (
|
||||
<Container service={service}>
|
||||
{[...Array(nBlocksGuess)].map((_, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Block key={i} label="spoolman.loading" />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (spoolData.error || spoolData.message) {
|
||||
return <Container service={service} error={spoolData?.error ?? spoolData} />;
|
||||
}
|
||||
|
||||
if (spoolData.length === 0) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="spoolman.noSpools" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.spoolIds?.length) {
|
||||
spoolData = spoolData.filter((spool) => widget.spoolIds.includes(spool.id));
|
||||
}
|
||||
|
||||
if (spoolData.length > 4) {
|
||||
spoolData = spoolData.slice(0, 4);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{spoolData.map((spool) => (
|
||||
<Block
|
||||
key={spool.id}
|
||||
label={spool.filament.name}
|
||||
value={t("common.percent", {
|
||||
value: (spool.remaining_weight / spool.initial_weight) * 100,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
spools: {
|
||||
endpoint: "spool",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -114,14 +114,14 @@ function extractCounts(responseJSON, fields) {
|
||||
}
|
||||
|
||||
export default async function suwayomiProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -11,8 +11,8 @@ export default function Component({ service }) {
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "device");
|
||||
|
||||
if (statsError) {
|
||||
return <Container service={service} error={statsError} />;
|
||||
if (statsError || statsData?.message) {
|
||||
return <Container service={service} error={statsError ?? statsData} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
|
||||
@@ -8,14 +8,14 @@ const proxyName = "tdarrProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function tdarrProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -11,14 +11,14 @@ const headerCacheKey = `${proxyName}__headers`;
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function transmissionProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -14,13 +14,13 @@ const prefixCacheKey = `${proxyName}__prefix`;
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
let widget = null;
|
||||
if (group === "unifi_console" && service === "unifi_console") {
|
||||
// info widget
|
||||
const index = req.query?.query ? JSON.parse(req.query.query).index : undefined;
|
||||
widget = await getPrivateWidgetOptions("unifi_console", index);
|
||||
const infowidgetIndex = req.query?.query ? JSON.parse(req.query.query).index : undefined;
|
||||
widget = await getPrivateWidgetOptions("unifi_console", infowidgetIndex);
|
||||
if (!widget) {
|
||||
logger.debug("Error retrieving settings for this Unifi widget");
|
||||
return null;
|
||||
@@ -32,7 +32,7 @@ async function getWidget(req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
widget = await getServiceWidget(group, service);
|
||||
widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -3,8 +3,8 @@ import { UrbackupServer } from "urbackup-server-api";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
|
||||
export default async function urbackupProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const { group, service, index } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service, index);
|
||||
|
||||
const server = new UrbackupServer({
|
||||
url: serviceWidget.url,
|
||||
|
||||
@@ -8,14 +8,14 @@ const proxyName = "watchtowerProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function watchtowerProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
|
||||
@@ -32,6 +32,7 @@ import gamedig from "./gamedig/widget";
|
||||
import gatus from "./gatus/widget";
|
||||
import ghostfolio from "./ghostfolio/widget";
|
||||
import gitea from "./gitea/widget";
|
||||
import gitlab from "./gitlab/widget";
|
||||
import glances from "./glances/widget";
|
||||
import gluetun from "./gluetun/widget";
|
||||
import gotify from "./gotify/widget";
|
||||
@@ -102,6 +103,7 @@ import sabnzbd from "./sabnzbd/widget";
|
||||
import scrutiny from "./scrutiny/widget";
|
||||
import sonarr from "./sonarr/widget";
|
||||
import speedtest from "./speedtest/widget";
|
||||
import spoolman from "./spoolman/widget";
|
||||
import stash from "./stash/widget";
|
||||
import stocks from "./stocks/widget";
|
||||
import strelaysrv from "./strelaysrv/widget";
|
||||
@@ -163,6 +165,7 @@ const widgets = {
|
||||
gatus,
|
||||
ghostfolio,
|
||||
gitea,
|
||||
gitlab,
|
||||
glances,
|
||||
gluetun,
|
||||
gotify,
|
||||
@@ -237,6 +240,7 @@ const widgets = {
|
||||
scrutiny,
|
||||
sonarr,
|
||||
speedtest,
|
||||
spoolman,
|
||||
stash,
|
||||
stocks,
|
||||
strelaysrv,
|
||||
|
||||
@@ -7,13 +7,13 @@ import getServiceWidget from "utils/config/service-helpers";
|
||||
const logger = createLogger("xteveProxyHandler");
|
||||
|
||||
export default async function xteveProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const { group, service, index } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
if (!api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
|
||||
Reference in new issue
Block a user