Merge branch 'dev' into feature/next-14

This commit is contained in:
shamoon committed 2024-11-24 10:44:49 -08:00
commit 2877d99ed2
215 files changed
+13540 -5081

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse, gro
className={classNames(
"bookmark-group",
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" : "flex-1 p-1",
layout?.header === false ? "flex-1 px-1 -my-1 overflow-hidden" : "flex-1 p-1 overflow-hidden",
)}
>
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
+4 -3
View File
@@ -13,6 +13,7 @@ export default function Item({ bookmark }) {
<a
href={bookmark.href}
title={bookmark.name}
rel="noreferrer"
target={bookmark.target ?? settings.target ?? "_blank"}
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
@@ -28,9 +29,9 @@ export default function Item({ bookmark }) {
)}
{!bookmark.icon && bookmark.abbr}
</div>
<div className="flex-1 flex items-center justify-between rounded-r-md bookmark-text">
<div className="flex-1 grow pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">
<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>
+6
View File
@@ -98,6 +98,12 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
setCurrentItemIndex(currentItemIndex - 1);
event.preventDefault();
} else if (
event.key === "ArrowRight" &&
results[currentItemIndex] &&
results[currentItemIndex].type === "searchSuggestion"
) {
setSearchString(results[currentItemIndex].name);
}
}
+29
View File
@@ -35,6 +35,35 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
// check mdi- or si- prefixed icons
const prefix = icon.split("-")[0];
if (prefix === "sh") {
const iconName = icon.replace("sh-", "").replace(".svg", "").replace(".png", "").replace(".webp", "");
let extension;
if (icon.endsWith(".svg")) {
extension = "svg";
} else if (icon.endsWith(".webp")) {
extension = "webp";
} else {
extension = "png";
}
return (
<Image
src={`https://cdn.jsdelivr.net/gh/selfhst/icons@main/${extension}/${iconName}.${extension}`}
width={width}
height={height}
style={{
width,
height,
objectFit: "contain",
maxHeight: "100%",
maxWidth: "100%",
}}
alt={alt}
/>
);
}
if (prefix in iconSetURLs) {
// default to theme setting
let iconName = icon.replace(`${prefix}-`, "").replace(".svg", "");
+4
View File
@@ -12,6 +12,10 @@ function displayData(data) {
export default function Error({ error }) {
const { t } = useTranslation();
if (typeof error === "string") {
error = { message: error }; // eslint-disable-line no-param-reassign
}
if (error?.data?.error) {
error = error.data.error; // eslint-disable-line no-param-reassign
}
+1 -1
View File
@@ -14,7 +14,7 @@ export function slugifyAndEncode(tabName) {
export default function Tab({ tab }) {
const { activeTab, setActiveTab } = useContext(TabContext);
const matchesTab = decodeURI(activeTab) === slugify(tab);
const matchesTab = decodeURIComponent(activeTab) === slugify(tab);
return (
<li
+1 -1
View File
@@ -25,7 +25,7 @@ export default function Uptime({ refresh = 1500 }) {
return (
<Resource
icon={FaRegClock}
value={t("common.uptime", { value: data.uptime })}
value={t("common.duration", { value: data.uptime })}
label={t("resources.uptime")}
percentage={percent}
/>
+91
View File
@@ -0,0 +1,91 @@
import useSWR from "swr";
import { useState } from "react";
import { useTranslation } from "next-i18next";
import { FaChartLine } from "react-icons/fa6";
import Error from "../widget/error";
import Container from "../widget/container";
import PrimaryText from "../widget/primary_text";
import WidgetIcon from "../widget/widget_icon";
import Raw from "../widget/raw";
export default function Widget({ options }) {
const { t, i18n } = useTranslation();
const [viewingPercentChange, setViewingPercentChange] = useState(false);
const { color } = options;
const { data, error } = useSWR(
`/api/widgets/stocks?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
);
if (error || data?.error) {
return <Error options={options} />;
}
if (!data) {
return (
<Container>
<WidgetIcon icon={FaChartLine} />
<PrimaryText>{t("stocks.loading")}...</PrimaryText>
</Container>
);
}
if (data) {
return (
<Container options={options} additionalClassNames="information-widget-stocks">
<Raw>
<button
type="button"
onClick={() => setViewingPercentChange(!viewingPercentChange)}
className="flex items-center w-full h-full hover:outline-none focus:outline-none"
>
<FaChartLine className="flex-none w-5 h-5 text-theme-800 dark:text-theme-200 mr-2" />
<div className="flex flex-wrap items-center gap-0.5">
{data.stocks.map(
(stock) =>
stock && (
<div
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>
{!viewingPercentChange ? (
<span
className={
color !== false
? `text-xs ${stock.percentChange < 0 ? "text-rose-300/70" : "text-emerald-300/70"}`
: "text-theme-800/70 dark:text-theme-200/50 text-xs"
}
>
{stock.currentPrice !== null
? t("common.number", {
value: stock.currentPrice,
style: "currency",
currency: "USD",
})
: t("widget.api_error")}
</span>
) : (
<span
className={
color !== false
? `text-xs ${stock.percentChange < 0 ? "text-rose-300/70" : "text-emerald-300/70"}`
: "text-theme-800/70 dark:text-theme-200/70 text-xs"
}
>
{stock.percentChange !== null ? `${stock.percentChange}%` : t("widget.api_error")}
</span>
)}
</div>
),
)}
</div>
</button>
</Raw>
</Container>
);
}
}
+1
View File
@@ -15,6 +15,7 @@ const widgetMappings = {
openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")),
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
stocks: dynamic(() => import("components/widgets/stocks/stocks")),
};
export default function Widget({ widget, style }) {
+1 -1
View File
@@ -8,7 +8,7 @@ export default function Document() {
name="description"
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
</Head>
<body>
@@ -48,8 +48,8 @@ export default async function handler(req, res) {
logger.error(`no pods found with namespace=${namespace} and labelSelector=${labelSelector}`);
return;
}
const someReady = pods.find((pod) => pod.status.phase === "Running");
const allReady = pods.every((pod) => pod.status.phase === "Running");
const someReady = pods.find((pod) => ["Succeeded", "Running"].includes(pod.status.phase));
const allReady = pods.every((pod) => ["Succeeded", "Running"].includes(pod.status.phase));
let status = "down";
if (allReady) {
status = "running";
+2 -2
View File
@@ -21,7 +21,7 @@ export default async function handler(req, res) {
if (!widget) {
logger.debug("Unknown proxy service type: %s", type);
return res.status(403).json({ error: "Unkown proxy service type" });
return res.status(403).json({ error: "Unknown proxy service type" });
}
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
@@ -107,7 +107,7 @@ export default async function handler(req, res) {
}
logger.debug("Unknown proxy service type: %s", type);
return res.status(403).json({ error: "Unkown proxy service type" });
return res.status(403).json({ error: "Unknown proxy service type" });
} catch (e) {
if (e) logger.error(e);
return res.status(500).send({ error: "Unexpected error" });
+2 -1
View File
@@ -22,11 +22,12 @@ export default async function handler(req, res) {
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return null;
});
if (!nodes) {
return res.status(500).send({
error: "unknown error",
error: "An error occurred while fetching nodes, check logs for more details.",
});
}
let cpuTotal = 0;
+1 -1
View File
@@ -2,7 +2,7 @@ import cachedFetch from "utils/proxy/cached-fetch";
export default async function handler(req, res) {
const { latitude, longitude, units, cache, timezone } = req.query;
const degrees = units === "imperial" ? "fahrenheit" : "celsius";
const degrees = units === "metric" ? "celsius" : "fahrenheit";
const timezeone = timezone ?? "auto";
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset&current_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
return res.send(await cachedFetch(apiUrl, cache));
+12 -3
View File
@@ -1,5 +1,9 @@
import { existsSync } from "fs";
import createLogger from "utils/logger";
const logger = createLogger("resources");
const si = require("systeminformation");
export default async function handler(req, res) {
@@ -23,26 +27,31 @@ export default async function handler(req, res) {
}
const fsSize = await si.fsSize();
logger.debug("fsSize:", JSON.stringify(fsSize));
return res.status(200).json({
drive: fsSize.find((fs) => fs.mount === target) ?? fsSize.find((fs) => fs.mount === "/"),
});
}
if (type === "memory") {
const memory = await si.mem();
logger.debug("memory:", JSON.stringify(memory));
return res.status(200).json({
memory: await si.mem(),
memory,
});
}
if (type === "cputemp") {
const cputemp = await si.cpuTemperature();
logger.debug("cputemp:", JSON.stringify(cputemp));
return res.status(200).json({
cputemp: await si.cpuTemperature(),
cputemp,
});
}
if (type === "uptime") {
const timeData = await si.time();
logger.debug("timeData:", JSON.stringify(timeData));
return res.status(200).json({
uptime: timeData.uptime,
});
+76
View File
@@ -0,0 +1,76 @@
import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";
export default async function handler(req, res) {
const { watchlist, provider, cache } = req.query;
if (!watchlist) {
return res.status(400).json({ error: "Missing watchlist" });
}
const watchlistArr = watchlist.split(",") || [watchlist];
if (!watchlistArr.length || watchlistArr[0] === "null" || !watchlistArr[0]) {
return res.status(400).json({ error: "Missing watchlist" });
}
if (watchlistArr.length > 8) {
return res.status(400).json({ error: "Max items in watchlist is 8" });
}
const hasDuplicates = new Set(watchlistArr).size !== watchlistArr.length;
if (hasDuplicates) {
return res.status(400).json({ error: "Watchlist contains duplicates" });
}
if (!provider) {
return res.status(400).json({ error: "Missing provider" });
}
if (provider !== "finnhub") {
return res.status(400).json({ error: "Invalid provider" });
}
const providersInConfig = getSettings()?.providers;
let apiKey;
Object.entries(providersInConfig).forEach(([key, val]) => {
if (key === provider) apiKey = val;
});
if (typeof apiKey === "undefined") {
return res.status(400).json({ error: "Missing or invalid API Key for provider" });
}
if (provider === "finnhub") {
// Finnhub allows up to 30 calls/second
// https://finnhub.io/docs/api/rate-limit
const results = await Promise.all(
watchlistArr.map(async (ticker) => {
if (!ticker) {
return { ticker: null, currentPrice: null, percentChange: null };
}
// https://finnhub.io/docs/api/quote
const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`;
// Finnhub free accounts allow up to 60 calls/minute
// https://finnhub.io/pricing
const { c, dp } = await cachedFetch(apiUrl, cache || 1);
// API sometimes returns 200, but values returned are `null`
if (c === null || dp === null) {
return { ticker, currentPrice: null, percentChange: null };
}
// Rounding percentage, but we want it back to a number for comparison
return { ticker, currentPrice: c.toFixed(2), percentChange: parseFloat(dp.toFixed(2)) };
}),
);
return res.send({
stocks: results,
});
}
return res.status(400).json({ error: "Invalid configuration" });
}
+1 -1
View File
@@ -393,7 +393,7 @@ function Home({ initialSettings }) {
<div
id="information-widgets"
className={classNames(
"flex flex-row flex-wrap justify-between",
"flex flex-row flex-wrap justify-between z-20",
headerStyles[headerStyle],
settings.cardBlur !== undefined &&
headerStyle === "boxed" &&
+1 -1
View File
@@ -1,6 +1,6 @@
---
# For configuration options and examples, please see:
# https://gethomepage.dev/latest/configs/bookmarks
# https://gethomepage.dev/configs/bookmarks
- Developer:
- Github:
+1 -1
View File
@@ -1,6 +1,6 @@
---
# For configuration options and examples, please see:
# https://gethomepage.dev/latest/configs/docker/
# https://gethomepage.dev/configs/docker/
# my-docker:
# host: 127.0.0.1
+1 -1
View File
@@ -1,6 +1,6 @@
---
# For configuration options and examples, please see:
# https://gethomepage.dev/latest/configs/services
# https://gethomepage.dev/configs/services
- My First Group:
- My First Service:
+1 -1
View File
@@ -1,6 +1,6 @@
---
# For configuration options and examples, please see:
# https://gethomepage.dev/latest/configs/settings
# https://gethomepage.dev/configs/settings
providers:
openweathermap: openweathermapapikey
+1 -1
View File
@@ -1,6 +1,6 @@
---
# For configuration options and examples, please see:
# https://gethomepage.dev/latest/configs/service-widgets
# https://gethomepage.dev/configs/service-widgets
- resources:
cpu: true
+60 -4
View File
@@ -198,6 +198,7 @@ export async function servicesFromKubernetes() {
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return null;
});
@@ -215,6 +216,7 @@ export async function servicesFromKubernetes() {
error.body,
error.response,
);
logger.debug(error);
}
return [];
@@ -231,6 +233,7 @@ export async function servicesFromKubernetes() {
error.body,
error.response,
);
logger.debug(error);
}
return [];
@@ -300,6 +303,7 @@ export async function servicesFromKubernetes() {
constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
} catch (e) {
logger.error("Error attempting k8s environment variable substitution.");
logger.debug(e);
}
return constructedService;
@@ -364,6 +368,9 @@ export function cleanServiceGroups(groups) {
repositoryId,
userEmail,
// beszel
systemId,
// calendar
firstDayInWeek,
integrations,
@@ -399,7 +406,10 @@ export function cleanServiceGroups(groups) {
expandOneStreamToTwoRows,
showEpisodeNumber,
// glances, pihole
// frigate
enableRecentEvents,
// glances, immich, mealie, pihole, pfsense
version,
// glances
@@ -408,7 +418,7 @@ export function cleanServiceGroups(groups) {
pointsLimit,
diskUnits,
// glances, customapi, iframe
// glances, customapi, iframe, prometheusmetric
refreshInterval,
// hdhomerun
@@ -435,6 +445,9 @@ export function cleanServiceGroups(groups) {
namespace,
podSelector,
// lubelogger
vehicleID,
// mjpeg
fit,
stream,
@@ -448,6 +461,9 @@ export function cleanServiceGroups(groups) {
// opnsense, pfsense
wan,
// prometheusmetric
metrics,
// proxmox
node,
@@ -457,6 +473,10 @@ export function cleanServiceGroups(groups) {
// sonarr, radarr
enableQueue,
// stocks
watchlist,
showUSMarketStatus,
// truenas
enablePools,
nasType,
@@ -464,8 +484,17 @@ export function cleanServiceGroups(groups) {
// unifi
site,
// vikunja
enableTaskList,
// wgeasy
threshold,
// technitium
range,
// spoolman
spoolIds,
} = cleanedService.widget;
let fieldsList = fields;
@@ -491,6 +520,10 @@ export function cleanServiceGroups(groups) {
if (repositoryId) cleanedService.widget.repositoryId = repositoryId;
}
if (type === "beszel") {
if (systemId) cleanedService.widget.systemId = systemId;
}
if (type === "coinmarketcap") {
if (currency) cleanedService.widget.currency = currency;
if (symbols) cleanedService.widget.symbols = symbols;
@@ -551,8 +584,8 @@ export function cleanServiceGroups(groups) {
if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost;
if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath;
}
if (["glances", "pihole"].includes(type)) {
if (version) cleanedService.widget.version = version;
if (["glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
if (version) cleanedService.widget.version = parseInt(version, 10);
}
if (type === "glances") {
if (metric) cleanedService.widget.metric = metric;
@@ -600,9 +633,32 @@ export function cleanServiceGroups(groups) {
cleanedService.widget.bitratePrecision = parseInt(bitratePrecision, 10);
}
}
if (type === "stocks") {
if (watchlist) cleanedService.widget.watchlist = watchlist;
if (showUSMarketStatus) cleanedService.widget.showUSMarketStatus = showUSMarketStatus;
}
if (type === "wgeasy") {
if (threshold !== undefined) cleanedService.widget.threshold = parseInt(threshold, 10);
}
if (type === "frigate") {
if (enableRecentEvents !== undefined) cleanedService.widget.enableRecentEvents = enableRecentEvents;
}
if (type === "technitium") {
if (range !== undefined) cleanedService.widget.range = range;
}
if (type === "lubelogger") {
if (vehicleID !== undefined) cleanedService.widget.vehicleID = parseInt(vehicleID, 10);
}
if (type === "vikunja") {
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
}
if (type === "prometheusmetric") {
if (metrics) cleanedService.widget.metrics = metrics;
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
}
if (type === "spoolman") {
if (spoolIds !== undefined) cleanedService.widget.spoolIds = spoolIds;
}
}
return cleanedService;
+29 -4
View File
@@ -3,6 +3,7 @@ import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
import validateWidgetData from "utils/proxy/validate-widget-data";
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
import { getSettings } from "utils/config/config";
import widgets from "widgets/widgets";
const logger = createLogger("credentialedProxyHandler");
@@ -24,14 +25,30 @@ export default async function credentialedProxyHandler(req, res, map) {
"Content-Type": "application/json",
};
if (widget.type === "coinmarketcap") {
if (widget.type === "stocks") {
const { providers } = getSettings();
if (widget.provider === "finnhub" && providers?.finnhub) {
headers["X-Finnhub-Token"] = `${providers?.finnhub}`;
}
} else if (widget.type === "coinmarketcap") {
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
} else if (widget.type === "gotify") {
headers["X-gotify-Key"] = `${widget.key}`;
} else if (
["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "tandoor", "pterodactyl"].includes(
widget.type,
)
[
"argocd",
"authentik",
"cloudflared",
"ghostfolio",
"headscale",
"linkwarden",
"mealie",
"netalertx",
"tailscale",
"tandoor",
"pterodactyl",
"vikunja",
].includes(widget.type)
) {
headers.Authorization = `Bearer ${widget.key}`;
} else if (widget.type === "truenas") {
@@ -71,6 +88,14 @@ export default async function credentialedProxyHandler(req, res, map) {
headers.Key = `${widget.key}`;
} else if (widget.type === "myspeed") {
headers.Password = `${widget.password}`;
} else if (widget.type === "esphome") {
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 -2
View File
@@ -23,7 +23,7 @@ export default async function genericProxyHandler(req, res, map) {
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
);
const headers = req.extraHeaders ?? widget.headers ?? {};
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
if (widget.username && widget.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
@@ -75,7 +75,13 @@ export default async function genericProxyHandler(req, res, map) {
url.port ? `:${url.port}` : "",
url.pathname,
);
return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url), resultData } });
return res.status(status).json({
error: {
message: "HTTP Error",
url: sanitizeErrorURL(url),
resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
},
});
}
return res.status(status).send(resultData);
+11 -5
View File
@@ -8,14 +8,18 @@ import widgets from "widgets/widgets";
const logger = createLogger("jsonrpcProxyHandler");
export async function sendJsonRpcRequest(url, method, params, username, password) {
export async function sendJsonRpcRequest(url, method, params, widget) {
const headers = {
"content-type": "application/json",
accept: "application/json",
};
if (username && password) {
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
if (widget?.username && widget?.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
}
if (widget?.key) {
headers.Authorization = `Bearer ${widget.key}`;
}
const client = new JSONRPCClient(async (rpcRequest) => {
@@ -67,6 +71,9 @@ export default async function jsonrpcProxyHandler(req, res) {
const widget = await getServiceWidget(group, service);
const api = widgets?.[widget.type]?.api;
const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method);
const params = mapping?.params ?? null;
if (!api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
@@ -74,8 +81,7 @@ export default async function jsonrpcProxyHandler(req, res) {
if (widget) {
const url = formatApiCall(api, { ...widget });
// eslint-disable-next-line no-unused-vars
const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
const [status, , data] = await sendJsonRpcRequest(url, method, params, widget);
return res.status(status).end(data);
}
}
+11 -3
View File
@@ -1,5 +1,8 @@
/* eslint-disable no-console */
import widgets from "widgets/widgets";
import createLogger from "utils/logger";
const logger = createLogger("validateWidgetData");
export default function validateWidgetData(widget, endpoint, data) {
let valid = true;
@@ -10,8 +13,13 @@ export default function validateWidgetData(widget, endpoint, data) {
try {
dataParsed = JSON.parse(data);
} catch (e) {
error = e;
valid = false;
try {
// try once more stripping whitespace
dataParsed = JSON.parse(data.toString().replace(/\s/g, ""));
} catch (e2) {
error = e || e2;
valid = false;
}
}
}
@@ -28,7 +36,7 @@ export default function validateWidgetData(widget, endpoint, data) {
}
if (!valid) {
console.warn(
logger.error(
`Invalid data for widget '${widget.type}' endpoint '${endpoint}':\nExpected:${mapping?.validate}\nParse error: ${
error ?? "none"
}\nData: ${JSON.stringify(data)}`,
+52
View File
@@ -0,0 +1,52 @@
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 { widget } = service;
if (!widget.fields) {
widget.fields = ["apps", "synced", "outOfSync", "healthy"];
}
const MAX_ALLOWED_FIELDS = 4;
if (widget.fields.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");
const appCounts = widget.fields.map((status) => {
if (status === "apps") {
return { status, count: appsData?.items?.length };
}
const count = appsData?.items?.filter(
(item) =>
item.status?.sync?.status.toLowerCase() === status.toLowerCase() ||
item.status?.health?.status.toLowerCase() === status.toLowerCase(),
).length;
return { status, count };
});
if (appsError) {
return <Container service={service} error={appsError} />;
}
if (!appsData) {
return (
<Container service={service}>
{appCounts.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} />
))}
</Container>
);
}
return (
<Container service={service}>
{appCounts.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} value={a.count} />
))}
</Container>
);
}
+14
View File
@@ -0,0 +1,14 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
applications: {
endpoint: "applications",
},
},
};
export default widget;
+4 -10
View File
@@ -39,21 +39,15 @@ export default function Component({ service }) {
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
<Block
label="audiobookshelf.podcastsDuration"
value={t("common.number", {
value: totalPodcastsDuration / 60,
maximumFractionDigits: 0,
style: "unit",
unit: "minute",
value={t("common.duration", {
value: totalPodcastsDuration,
})}
/>
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
<Block
label="audiobookshelf.booksDuration"
value={t("common.number", {
value: totalBooksDuration / 60,
maximumFractionDigits: 0,
style: "unit",
unit: "minute",
value={t("common.duration", {
value: totalBooksDuration,
})}
/>
</Container>
+60
View File
@@ -0,0 +1,60 @@
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 { systemId } = widget;
const { data: systems, error: systemsError } = useWidgetAPI(widget, "systems");
const MAX_ALLOWED_FIELDS = 4;
if (!widget.fields?.length > 0) {
widget.fields = systemId ? ["name", "status", "cpu", "memory"] : ["systems", "up"];
}
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (systemsError) {
return <Container service={service} error={systemsError} />;
}
if (!systems) {
return (
<Container service={service}>
<Block label="beszel.systems" />
<Block label="beszel.up" />
</Container>
);
}
if (systemId) {
const system = systems.items.find((item) => item.id === systemId);
return (
<Container service={service}>
<Block label="beszel.name" value={system.name} />
<Block label="beszel.status" value={t(`beszel.${system.status}`)} />
<Block label="beszel.updated" value={t("common.relativeDate", { value: system.updated })} />
<Block label="beszel.cpu" value={t("common.percent", { value: system.info.cpu, maximumFractionDigits: 2 })} />
<Block label="beszel.memory" value={t("common.percent", { value: system.info.mp, maximumFractionDigits: 2 })} />
<Block label="beszel.disk" value={t("common.percent", { value: system.info.dp, maximumFractionDigits: 2 })} />
<Block label="beszel.network" value={t("common.percent", { value: system.info.b, maximumFractionDigits: 2 })} />
</Container>
);
}
const upTotal = systems.items.filter((item) => item.status === "up").length;
return (
<Container service={service}>
<Block label="beszel.systems" value={systems.totalItems} />
<Block label="beszel.up" value={`${upTotal} / ${systems.totalItems}`} />
</Container>
);
}
+99
View File
@@ -0,0 +1,99 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
import createLogger from "utils/logger";
const proxyName = "beszelProxyHandler";
const tokenCacheKey = `${proxyName}__token`;
const logger = createLogger(proxyName);
async function login(loginUrl, username, password, service) {
const authResponse = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify({ identity: username, password }),
headers: {
"Content-Type": "application/json",
},
});
const status = authResponse[0];
let data = authResponse[2];
try {
data = JSON.parse(Buffer.from(authResponse[2]).toString());
if (status === 200) {
cache.put(`${tokenCacheKey}.${service}`, data.token);
}
} catch (e) {
logger.error(`Error ${status} logging into beszel`, JSON.stringify(authResponse[2]));
}
return [status, data.token ?? data];
}
export default async function beszelProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
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 status;
let data;
let token = cache.get(`${tokenCacheKey}.${service}`);
if (!token) {
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
return res.status(status).send(token);
}
}
[status, , data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (status === 403) {
logger.debug(`HTTP ${status} retrieving data from npm api, 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}`);
return res.status(status).send(data);
}
// eslint-disable-next-line no-unused-vars
[status, , data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
}
if (status !== 200) {
return res.status(status).send(data);
}
return res.send(data);
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}
+14
View File
@@ -0,0 +1,14 @@
import beszelProxyHandler from "./proxy";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: beszelProxyHandler,
mappings: {
systems: {
endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated",
},
},
};
export default widget;
+1 -1
View File
@@ -16,7 +16,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
key={`event-${event.title}-${event.date}-${event.additional}`}
>
{showDateColumn && (
<span className="ml-2 w-10">
<span className="ml-2 w-12">
<span>
{(showDate || showTime) &&
event.date
+1 -1
View File
@@ -21,7 +21,7 @@ export default async function calendarProxyHandler(req, res) {
if (contentType) res.setHeader("Content-Type", contentType);
if (status !== 200) {
logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
logger.debug(`HTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
return res.status(status).send(data);
}
+16
View File
@@ -2,12 +2,14 @@ import dynamic from "next/dynamic";
const components = {
adguard: dynamic(() => import("./adguard/component")),
argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
authentik: dynamic(() => import("./authentik/component")),
autobrr: dynamic(() => import("./autobrr/component")),
azuredevops: dynamic(() => import("./azuredevops/component")),
bazarr: dynamic(() => import("./bazarr/component")),
beszel: dynamic(() => import("./beszel/component")),
caddy: dynamic(() => import("./caddy/component")),
calendar: dynamic(() => import("./calendar/component")),
calibreweb: dynamic(() => import("./calibreweb/component")),
@@ -19,6 +21,7 @@ const components = {
iframe: dynamic(() => import("./iframe/component")),
customapi: dynamic(() => import("./customapi/component")),
deluge: dynamic(() => import("./deluge/component")),
develancacheui: dynamic(() => import("./develancacheui/component")),
diskstation: dynamic(() => import("./diskstation/component")),
downloadstation: dynamic(() => import("./downloadstation/component")),
docker: dynamic(() => import("./docker/component")),
@@ -29,16 +32,19 @@ const components = {
fileflows: dynamic(() => import("./fileflows/component")),
flood: dynamic(() => import("./flood/component")),
freshrss: dynamic(() => import("./freshrss/component")),
frigate: dynamic(() => import("./frigate/component")),
fritzbox: dynamic(() => import("./fritzbox/component")),
gamedig: dynamic(() => import("./gamedig/component")),
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")),
grafana: dynamic(() => import("./grafana/component")),
hdhomerun: dynamic(() => import("./hdhomerun/component")),
headscale: dynamic(() => import("./headscale/component")),
peanut: dynamic(() => import("./peanut/component")),
homeassistant: dynamic(() => import("./homeassistant/component")),
homebox: dynamic(() => import("./homebox/component")),
@@ -53,6 +59,9 @@ const components = {
komga: dynamic(() => import("./komga/component")),
kopia: dynamic(() => import("./kopia/component")),
lidarr: dynamic(() => import("./lidarr/component")),
linkwarden: dynamic(() => import("./linkwarden/component")),
lubelogger: dynamic(() => import("./lubelogger/component")),
mailcow: dynamic(() => import("./mailcow/component")),
mastodon: dynamic(() => import("./mastodon/component")),
mealie: dynamic(() => import("./mealie/component")),
medusa: dynamic(() => import("./medusa/component")),
@@ -88,6 +97,7 @@ const components = {
plex: dynamic(() => import("./plex/component")),
portainer: dynamic(() => import("./portainer/component")),
prometheus: dynamic(() => import("./prometheus/component")),
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
prowlarr: dynamic(() => import("./prowlarr/component")),
proxmox: dynamic(() => import("./proxmox/component")),
pterodactyl: dynamic(() => import("./pterodactyl/component")),
@@ -102,12 +112,16 @@ 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")),
swagdashboard: dynamic(() => import("./swagdashboard/component")),
suwayomi: dynamic(() => import("./suwayomi/component")),
tailscale: dynamic(() => import("./tailscale/component")),
tandoor: dynamic(() => import("./tandoor/component")),
tautulli: dynamic(() => import("./tautulli/component")),
technitium: dynamic(() => import("./technitium/component")),
tdarr: dynamic(() => import("./tdarr/component")),
traefik: dynamic(() => import("./traefik/component")),
transmission: dynamic(() => import("./transmission/component")),
@@ -118,10 +132,12 @@ const components = {
uptimekuma: dynamic(() => import("./uptimekuma/component")),
uptimerobot: dynamic(() => import("./uptimerobot/component")),
urbackup: dynamic(() => import("./urbackup/component")),
vikunja: dynamic(() => import("./vikunja/component")),
watchtower: dynamic(() => import("./watchtower/component")),
wgeasy: dynamic(() => import("./wgeasy/component")),
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
xteve: dynamic(() => import("./xteve/component")),
zabbix: dynamic(() => import("./zabbix/component")),
};
export default components;
+18
View File
@@ -9,6 +9,11 @@ function getValue(field, data) {
let lastField = field;
let key = "";
// Support APIs that return arrays or scalars directly.
if (typeof field === "undefined") {
return value;
}
while (typeof lastField === "object") {
key = Object.keys(lastField)[0] ?? null;
@@ -27,6 +32,16 @@ function getValue(field, data) {
return value[lastField] ?? null;
}
function getSize(data) {
if (Array.isArray(data) || typeof data === "string") {
return data.length;
} else if (typeof data === "object" && data !== null) {
return Object.keys(data).length;
}
return NaN;
}
function formatValue(t, mapping, rawValue) {
let value = rawValue;
@@ -85,6 +100,9 @@ function formatValue(t, mapping, rawValue) {
numeric: mapping?.numeric,
});
break;
case "size":
value = t("common.number", { value: getSize(value) });
break;
case "text":
default:
// nothing
+39
View File
@@ -0,0 +1,39 @@
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: downloadStatsData, error: downloadStatsError } = useWidgetAPI(widget, "stats");
if (downloadStatsError) {
return <Container service={service} error={downloadStatsError} />;
}
if (!downloadStatsData) {
return (
<Container service={service}>
<Block label="develancacheui.cachehitbytes" />
<Block label="develancacheui.cachemissbytes" />
</Container>
);
}
return (
<Container service={service}>
<Block
label="develancacheui.cachehitbytes"
value={t("common.bytes", { value: downloadStatsData.totalCacheHitBytes })}
/>
<Block
label="develancacheui.cachemissbytes"
value={t("common.bytes", { value: downloadStatsData.totalCacheMissBytes })}
/>
</Container>
);
}
+14
View File
@@ -0,0 +1,14 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
stats: {
endpoint: "DownloadStats/GetTotalDownloadStats",
},
},
};
export default widget;
+2 -2
View File
@@ -1,8 +1,8 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/ping",
proxyHandler: genericProxyHandler,
proxyHandler: credentialedProxyHandler,
};
export default widget;
+70
View File
@@ -0,0 +1,70 @@
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, error } = useWidgetAPI(widget, "stats");
const { data: eventsData, error: eventsError } = useWidgetAPI(widget, "events");
if (error) {
return <Container service={service} error={error} />;
}
if (eventsError) {
return <Container service={service} error={eventsError} />;
}
if (!data || !eventsData) {
return (
<Container service={service}>
<Block label="frigate.cameras" />
<Block label="frigate.uptime" />
<Block label="frigate.version" />
</Container>
);
}
return (
<>
<Container service={service}>
<Block
label="frigate.cameras"
value={t("common.number", {
value: data.num_cameras,
})}
/>
<Block
label="frigate.uptime"
value={t("common.duration", {
value: data.uptime,
})}
/>
<Block label="frigate.version" value={data.version} />
</Container>
{widget.enableRecentEvents &&
eventsData?.map((event) => (
<div
key={event.id}
className="text-theme-700 dark:text-theme-200 _relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex"
>
<div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
<div className="absolute w-full h-4 whitespace-nowrap text-ellipsis overflow-hidden text-left">
{event.camera} ({event.label} {t("common.percent", { value: event.score * 100 })})
</div>
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
{t("common.date", {
value: event.start_time,
formatParams: { value: { timeStyle: "short", dateStyle: "medium" } },
})}
</div>
</div>
))}
</>
);
}
+38
View File
@@ -0,0 +1,38 @@
import { asJson } from "utils/proxy/api-helpers";
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
stats: {
endpoint: "stats",
map: (data) => {
const jsonData = asJson(data);
return {
num_cameras: jsonData?.cameras !== undefined ? Object.keys(jsonData?.cameras).length : 0,
uptime: jsonData?.service?.uptime,
version: jsonData?.service.version,
};
},
},
events: {
endpoint: "events",
map: (data) =>
asJson(data)
.slice(0, 5)
.map((event) => ({
id: event.id,
camera: event.camera,
label: event.label,
start_time: new Date(event.start_time * 1000),
thumbnail: event.thumbnail,
score: event.data.score,
type: event.data.type,
})),
},
},
};
export default widget;
+1 -1
View File
@@ -44,7 +44,7 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
<Block label="fritzbox.uptime" value={t("common.uptime", { value: fritzboxData.uptime })} />
<Block label="fritzbox.uptime" value={t("common.duration", { value: fritzboxData.uptime })} />
<Block label="fritzbox.maxDown" value={t("common.byterate", { value: fritzboxData.maxDown / 8, decimals: 1 })} />
<Block label="fritzbox.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, decimals: 1 })} />
<Block label="fritzbox.down" value={t("common.byterate", { value: fritzboxData.down, decimals: 1 })} />
+4 -2
View File
@@ -1,9 +1,10 @@
import { GameDig } from "gamedig";
import createLogger from "utils/logger";
import getServiceWidget from "utils/config/service-helpers";
const proxyName = "gamedigProxyHandler";
const logger = createLogger(proxyName);
const gamedig = require("gamedig");
export default async function gamedigProxyHandler(req, res) {
const { group, service } = req.query;
@@ -11,11 +12,12 @@ export default async function gamedigProxyHandler(req, res) {
const url = new URL(serviceWidget.url);
try {
const serverData = await gamedig.query({
const serverData = await GameDig.query({
type: serviceWidget.serverType,
host: url.hostname,
port: url.port,
givenPortOnly: true,
checkOldIDs: true,
});
res.status(200).send({
+2
View File
@@ -6,7 +6,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
function getPerformancePercent(t, performanceRange) {
// ghostfolio v2.79.0 changed to grossPerformancePercentage
// ghostfolio v2.106.0 changed to netPerformancePercentageWithCurrencyEffect
const percent =
performanceRange.performance.netPerformancePercentageWithCurrencyEffect ??
performanceRange.performance.grossPerformancePercentage ??
performanceRange.performance.currentGrossPerformancePercent;
return `${percent > 0 ? "+" : ""}${t("common.percent", {
+36
View File
@@ -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>
);
}
+13
View File
@@ -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;
+2 -2
View File
@@ -58,7 +58,7 @@ export default function Component({ service }) {
<div className="text-xs opacity-50">
{t(diskUnits, {
value: fsData.used,
maximumFractionDigits: 0,
maximumFractionDigits: 1,
})}{" "}
{t("resources.used")}
</div>
@@ -79,7 +79,7 @@ export default function Component({ service }) {
<div className="text-xs opacity-50">
{t(diskUnits, {
value: fsData.used,
maximumFractionDigits: 0,
maximumFractionDigits: 1,
})}{" "}
{t("resources.used")}
</div>
+2 -2
View File
@@ -1,8 +1,8 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/v1/{endpoint}",
proxyHandler: genericProxyHandler,
proxyHandler: credentialedProxyHandler,
mappings: {
ip: {
+43
View File
@@ -0,0 +1,43 @@
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: nodeData, error: nodeError } = useWidgetAPI(widget, "node");
if (nodeError) {
return <Container service={service} error={nodeError} />;
}
if (!nodeData) {
return (
<Container service={service}>
<Block label="headscale.name" />
<Block label="headscale.address" />
<Block label="headscale.last_seen" />
<Block label="headscale.status" />
</Container>
);
}
const {
givenName,
ipAddresses: [address],
lastSeen,
online,
} = nodeData.node;
return (
<Container service={service}>
<Block label="headscale.name" value={givenName} />
<Block label="headscale.address" value={address} />
<Block label="headscale.last_seen" value={t("common.relativeDate", { value: lastSeen })} />
<Block label="headscale.status" value={t(online ? "headscale.online" : "headscale.offline")} />
</Container>
);
}
+14
View File
@@ -0,0 +1,14 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v1/{endpoint}/{nodeId}",
proxyHandler: credentialedProxyHandler,
mappings: {
node: {
endpoint: "node",
},
},
};
export default widget;
+13 -4
View File
@@ -8,10 +8,19 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: versionData, error: versionError } = useWidgetAPI(widget, "version");
// see https://github.com/gethomepage/homepage/issues/2282
const endpoint = versionData?.major >= 1 && versionData?.minor > 84 ? "statistics" : "stats";
const { data: immichData, error: immichError } = useWidgetAPI(widget, endpoint);
const { version = 1 } = widget;
const versionEndpoint = version === 2 ? "version_v2" : "version";
const { data: versionData, error: versionError } = useWidgetAPI(widget, versionEndpoint);
let statsEndpoint = version === 2 ? "statistics_v2" : "stats";
if (version === 1) {
// see https://github.com/gethomepage/homepage/issues/2282
statsEndpoint =
versionData?.major > 1 || (versionData?.major === 1 && versionData?.minor > 84) ? "statistics" : "stats";
}
const { data: immichData, error: immichError } = useWidgetAPI(widget, statsEndpoint);
if (immichError || versionError || immichData?.statusCode === 401) {
return <Container service={service} error={immichData ?? immichError ?? versionError} />;
+10 -4
View File
@@ -1,18 +1,24 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/server-info/{endpoint}",
api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
version: {
endpoint: "version",
endpoint: "server-info/version",
},
statistics: {
endpoint: "statistics",
endpoint: "server-info/statistics",
},
stats: {
endpoint: "stats",
endpoint: "server-info/stats",
},
version_v2: {
endpoint: "server/version",
},
statistics_v2: {
endpoint: "server/statistics",
},
},
};
+55
View File
@@ -0,0 +1,55 @@
import React, { useState, useEffect } from "react";
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 { widget } = service;
const [stats, setStats] = useState({
totalLinks: null,
collections: { total: null },
tags: { total: null },
});
const { data: collectionsStatsData, error: collectionsStatsError } = useWidgetAPI(widget, "collections");
const { data: tagsStatsData, error: tagsStatsError } = useWidgetAPI(widget, "tags");
useEffect(() => {
if (collectionsStatsData?.response && tagsStatsData?.response) {
setStats({
// eslint-disable-next-line no-underscore-dangle
totalLinks: collectionsStatsData.response.reduce((sum, collection) => sum + (collection._count?.links || 0), 0),
collections: {
total: collectionsStatsData.response.length,
},
tags: {
total: tagsStatsData.response.length,
},
});
}
}, [collectionsStatsData, tagsStatsData]);
if (collectionsStatsError || tagsStatsError) {
return <Container service={service} error={collectionsStatsError || tagsStatsError} />;
}
if (!tagsStatsData || !collectionsStatsData) {
return (
<Container service={service}>
<Block label="linkwarden.links" />
<Block label="linkwarden.collections" />
<Block label="linkwarden.tags" />
</Container>
);
}
return (
<Container service={service}>
<Block label="linkwarden.links" value={stats.totalLinks} />
<Block label="linkwarden.collections" value={stats.collections.total} />
<Block label="linkwarden.tags" value={stats.tags.total} />
</Container>
);
}
+17
View File
@@ -0,0 +1,17 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
collections: {
endpoint: "collections",
},
tags: {
endpoint: "tags",
},
},
};
export default widget;
+65
View File
@@ -0,0 +1,65 @@
import { useTranslation } from "react-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;
let { data: vehicleInfo, error } = useWidgetAPI(widget, "vehicleinfo");
if (error) {
return <Container service={service} error={error} />;
}
if (!vehicleInfo) {
return (
<Container service={service}>
<Block label="lubelogger.vehicles" />
<Block label="lubelogger.serviceRecords" />
<Block label="lubelogger.reminders" />
</Container>
);
}
const { vehicleID } = widget;
if (vehicleID) {
vehicleInfo = vehicleInfo.filter((v) => v.vehicleData.id === vehicleID);
}
const totalReminders = vehicleInfo.reduce(
(acc, val) => acc + val.veryUrgentReminderCount + val.urgentReminderCount + val.notUrgentReminderCount,
0,
);
const totalServiceRecords = vehicleInfo.reduce((acc, val) => acc + val.serviceRecordCount, 0);
if (vehicleID) {
if (vehicleInfo.length === 0) {
error = { message: "Vehicle not found" };
return <Container service={service} error={error} />;
}
const nextReminder = vehicleInfo[0].nextReminder
? t("common.date", { value: vehicleInfo[0].nextReminder.dueDate })
: t("lubelogger.none");
return (
<Container service={service}>
<Block
label="lubelogger.vehicle"
value={`${vehicleInfo[0].vehicleData.year} ${vehicleInfo[0].vehicleData.model}`}
/>
<Block label="lubelogger.serviceRecords" value={totalServiceRecords} />
<Block label="lubelogger.reminders" value={totalReminders} />
<Block label="lubelogger.nextReminder" value={nextReminder} />
</Container>
);
}
return (
<Container service={service}>
<Block label="lubelogger.vehicles" value={vehicleInfo.length} />
<Block label="lubelogger.serviceRecords" value={totalServiceRecords} />
<Block label="lubelogger.reminders" value={totalReminders} />
</Container>
);
}
+14
View File
@@ -0,0 +1,14 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
vehicleinfo: {
endpoint: "vehicle/info",
},
},
};
export default widget;
+40
View File
@@ -0,0 +1,40 @@
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: resultData, error: resultError } = useWidgetAPI(widget, "domains");
if (resultError || (resultData && Object.keys(resultData).length === 0)) {
return <Container service={service} error={resultError ?? { message: "No domains found" }} />;
}
if (!resultData) {
return (
<Container service={service}>
<Block label="mailcow.mailboxes" />
<Block label="mailcow.aliases" />
<Block label="mailcow.quarantined" />
</Container>
);
}
const domains = resultData.length;
const mailboxes = resultData.reduce((acc, val) => acc + parseInt(val.mboxes_in_domain, 10), 0);
const mails = resultData.reduce((acc, val) => acc + parseInt(val.msgs_total, 10), 0);
const storage = resultData.reduce((acc, val) => acc + parseInt(val.bytes_total, 10), 0);
return (
<Container service={service}>
<Block label="mailcow.domains" value={t("common.number", { value: domains })} />
<Block label="mailcow.mailboxes" value={t("common.number", { value: mailboxes })} />
<Block label="mailcow.mails" value={t("common.number", { value: mails })} />
<Block label="mailcow.storage" value={t("common.bytes", { value: storage })} />
</Container>
);
}
+14
View File
@@ -0,0 +1,14 @@
import credentialedProxyHandler from "../../utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v1/get/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
domains: {
endpoint: "domain/all",
},
},
};
export default widget;
+12 -10
View File
@@ -1,17 +1,20 @@
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 version = widget.version ?? 1;
const { data, error } = useWidgetAPI(widget, version === 1 ? "statisticsv1" : "statisticsv2");
const { data: mealieData, error: mealieError } = useWidgetAPI(widget);
if (mealieError || mealieData?.statusCode === 401) {
return <Container service={service} error={mealieError ?? mealieData} />;
if (error) {
return <Container service={service} error={error} />;
}
if (!mealieData) {
if (!data) {
return (
<Container service={service}>
<Block label="mealie.recipes" />
@@ -21,13 +24,12 @@ export default function Component({ service }) {
</Container>
);
}
return (
<Container service={service}>
<Block label="mealie.recipes" value={mealieData.totalRecipes} />
<Block label="mealie.users" value={mealieData.totalUsers} />
<Block label="mealie.categories" value={mealieData.totalCategories} />
<Block label="mealie.tags" value={mealieData.totalTags} />
<Block label="mealie.recipes" value={t("common.number", { value: data.totalRecipes })} />
<Block label="mealie.users" value={t("common.number", { value: data.totalUsers })} />
<Block label="mealie.categories" value={t("common.number", { value: data.totalCategories })} />
<Block label="mealie.tags" value={t("common.number", { value: data.totalTags })} />
</Container>
);
}
+10 -1
View File
@@ -1,8 +1,17 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/groups/statistics",
api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
statisticsv1: {
endpoint: "groups/statistics",
},
statisticsv2: {
endpoint: "households/statistics",
},
},
};
export default widget;
+1 -1
View File
@@ -10,7 +10,7 @@ const widget = {
validate: ["cpu-load", "free-memory", "total-memory", "uptime"],
},
leases: {
endpoint: "ip/dhcp-server/lease",
endpoint: "ip/dhcp-server/lease?.proplist=address",
},
},
};
+2 -2
View File
@@ -1,8 +1,8 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/php/server/devices.php?action=getDevicesTotals",
proxyHandler: genericProxyHandler,
proxyHandler: credentialedProxyHandler,
mappings: {
data: {
+2 -2
View File
@@ -21,8 +21,8 @@ export default function Component({ service }) {
);
}
const enabled = infoData.filter((c) => c.enabled === 1).length;
const disabled = infoData.filter((c) => c.enabled === 0).length;
const enabled = infoData.filter((c) => !!c.enabled).length;
const disabled = infoData.filter((c) => !c.enabled).length;
const total = infoData.length;
return (
+6 -7
View File
@@ -30,7 +30,7 @@ async function login(loginUrl, username, password, service) {
cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - 5 * 60 * 1000); // expiration -5 minutes
}
} catch (e) {
logger.error(`Error ${status} logging into npm`, authResponse[2]);
logger.error(`Error ${status} logging into npm`, JSON.stringify(authResponse[2]));
}
return [status, data.token ?? data];
}
@@ -50,19 +50,18 @@ export default async function npmProxyHandler(req, res) {
const loginUrl = `${widget.url}/api/tokens`;
let status;
let contentType;
let data;
let token = cache.get(`${tokenCacheKey}.${service}`);
if (!token) {
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
return res.status(status).send(token);
}
}
[status, contentType, data] = await httpProxy(url, {
[status, , data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -71,17 +70,17 @@ export default async function npmProxyHandler(req, res) {
});
if (status === 403) {
logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
cache.del(`${tokenCacheKey}.${service}`);
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
return res.status(status).send(data);
}
// eslint-disable-next-line no-unused-vars
[status, contentType, data] = await httpProxy(url, {
[status, , data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
+6
View File
@@ -4,6 +4,12 @@ const widget = {
api: "{url}/jsonrpc",
proxyHandler: jsonrpcProxyHandler,
allowedEndpoints: /status/,
mappings: {
status: {
endpoint: "status",
},
},
};
export default widget;
+1 -1
View File
@@ -138,7 +138,7 @@ export default async function omadaProxyHandler(req, res) {
const sitesResponseData = JSON.parse(data);
if (status !== 200 || sitesResponseData.errorCode > 0) {
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
return res
.status(status)
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
+1 -1
View File
@@ -20,7 +20,7 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="openwrt.uptime" value={t("common.uptime", { value: uptime })} />
<Block label="openwrt.uptime" value={t("common.duration", { value: uptime })} />
<Block label="openwrt.cpuLoad" value={cpuLoad} />
</Container>
);
+16 -11
View File
@@ -9,8 +9,12 @@ export default function Component({ service }) {
const { widget } = service;
const { data: systemData, error: systemError } = useWidgetAPI(widget, "system");
const { data: interfaceData, error: interfaceError } = useWidgetAPI(widget, "interface");
const version = widget.version ?? 1;
const { data: systemData, error: systemError } = useWidgetAPI(widget, version === 1 ? "system" : "systemv2");
const { data: interfaceData, error: interfaceError } = useWidgetAPI(
widget,
version === 1 ? "interface" : "interfacev2",
);
const showWanIP = widget.fields?.filter((f) => f !== "wanIP").length <= 4 && widget.fields?.includes("wanIP");
const showDiskUsage = widget.fields?.filter((f) => f !== "disk").length <= 4 && widget.fields?.includes("disk");
@@ -34,14 +38,20 @@ export default function Component({ service }) {
}
const wan = interfaceData.data.filter((l) => l.hwif === widget.wan)[0];
let memUsage = systemData?.data.mem_usage;
let diskUsage = systemData.data.disk_usage;
if (version === 1) {
memUsage *= 100;
diskUsage *= 100;
}
return (
<Container service={service}>
<Block label="pfsense.load" value={systemData.data.load_avg[0]} />
<Block
label="pfsense.memory"
value={t("common.percent", { value: (systemData.data.mem_usage * 100).toFixed(2) })}
label="pfsense.load"
value={version === 1 ? systemData.data.load_avg[0] : systemData.data.cpu_load_avg[0]}
/>
<Block label="pfsense.memory" value={t("common.percent", { value: memUsage.toFixed(2) })} />
<Block
label="pfsense.temp"
value={t("common.number", { value: systemData.data.temp_c, style: "unit", unit: "celsius" })}
@@ -57,12 +67,7 @@ export default function Component({ service }) {
}
/>
{showWanIP && <Block label="pfsense.wanIP" value={wan.ipaddr} />}
{showDiskUsage && (
<Block
label="pfsense.disk"
value={t("common.percent", { value: (systemData.data.disk_usage * 100).toFixed(2) })}
/>
)}
{showDiskUsage && <Block label="pfsense.disk" value={t("common.percent", { value: diskUsage.toFixed(2) })} />}
</Container>
);
}
+11 -3
View File
@@ -1,16 +1,24 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v1/{endpoint}",
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
system: {
endpoint: "status/system",
endpoint: "v1/status/system",
validate: ["data"],
},
interface: {
endpoint: "status/interface",
endpoint: "v1/status/interface",
validate: ["data"],
},
systemv2: {
endpoint: "v2/status/system",
validate: ["data"],
},
interfacev2: {
endpoint: "v2/status/interfaces?limit=0&offset=0",
validate: ["data"],
},
},
+5
View File
@@ -32,6 +32,11 @@ export default async function photoprismProxyHandler(req, res) {
username: widget.username,
password: widget.password,
});
} else if (widget.key) {
params.headers.Authorization = `Bearer ${widget.key}`;
params.body = JSON.stringify({
authToken: widget.key,
});
}
const [status, contentType, data] = await httpProxy(url, params);
+11 -7
View File
@@ -57,23 +57,27 @@ export default async function piholeProxyHandler(req, res) {
}
// pihole v6
if (!cache.get(`${sessionSIDCacheKey}.${service}`)) {
if (!cache.get(`${sessionSIDCacheKey}.${service}`) && widget.key) {
await login(widget, service);
}
const sid = cache.get(`${sessionSIDCacheKey}.${service}`);
if (!sid) {
if (widget.key && !sid) {
return res.status(500).json({ error: "Failed to authenticate with Pi-hole" });
}
try {
logger.debug("Calling Pi-hole API endpoint: %s", endpoint);
const headers = {
"Content-Type": "application/json",
};
if (sid) {
headers["X-FTL-SID"] = sid;
} else {
logger.debug("Pi-hole request is unauthenticated");
}
[status, , data] = await httpProxy(formatApiCall(widgets[widget.type].api, { ...widget, endpoint }), {
headers: {
"Content-Type": "application/json",
"X-FTL-SID": sid,
},
headers,
});
if (status !== 200) {
+116
View File
@@ -0,0 +1,116 @@
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";
function formatValue(t, metric, rawValue) {
if (!metric?.format) return rawValue;
if (!rawValue) return "-";
let value = rawValue;
// Scale the value. Accepts either a number to multiply by or a string
// like "12/345".
const scale = metric?.format?.scale;
if (typeof scale === "number") {
value *= scale;
} else if (typeof scale === "string" && scale.includes("/")) {
const parts = scale.split("/");
const numerator = parts[0] ? parseFloat(parts[0]) : 1;
const denominator = parts[1] ? parseFloat(parts[1]) : 1;
value = (value * numerator) / denominator;
} else {
value = parseFloat(value);
}
// Format the value using a known type and optional options.
switch (metric?.format?.type) {
case "text":
break;
default:
value = t(`common.${metric.format.type}`, { value, ...metric.format?.options });
}
// Apply fixed prefix.
const prefix = metric?.format?.prefix;
if (prefix) {
value = `${prefix}${value}`;
}
// Apply fixed suffix.
const suffix = metric?.format?.suffix;
if (suffix) {
value = `${value}${suffix}`;
}
return value;
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { metrics = [], refreshInterval = 10000 } = widget;
let prometheusmetricError;
const prometheusmetricData = new Map(
metrics.slice(0, 4).map((metric) => {
// disable the rule that hooks should not be called from a callback,
// because we don't need a strong guarantee of hook execution order here.
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: resultData, error: resultError } = useWidgetAPI(widget, "query", {
query: metric.query,
refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
});
if (resultError) {
prometheusmetricError = resultError;
}
return [metric.key ?? metric.label, resultData];
}),
);
if (prometheusmetricError) {
return <Container service={service} error={prometheusmetricError} />;
}
if (!prometheusmetricData) {
return (
<Container service={service}>
{metrics.slice(0, 4).map((item) => (
<Block label={item.label} key={item.label} />
))}
</Container>
);
}
function getResultValue(data) {
// Fetches the first metric result from the Prometheus query result data.
// The first element in the result value is the timestamp which is ignored here.
const resultType = data?.data?.resultType;
const result = data?.data?.result;
switch (resultType) {
case "vector":
return result?.[0]?.value?.[1];
case "scalar":
return result?.[1];
default:
return "";
}
}
return (
<Container service={service}>
{metrics.map((metric) => (
<Block
label={metric.label}
key={metric.key ?? metric.label}
value={formatValue(t, metric, getResultValue(prometheusmetricData.get(metric.key ?? metric.label)))}
/>
))}
</Container>
);
}
+16
View File
@@ -0,0 +1,16 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
query: {
method: "GET",
endpoint: "query",
params: ["query"],
},
},
};
export default widget;
@@ -29,7 +29,7 @@ export default function Component({ service }) {
);
}
const datastoreUsage = (datastoreData.data[0].used / datastoreData.data[0].total) * 100;
const datastoreUsage = datastoreData.data ? (datastoreData.data[0].used / datastoreData.data[0].total) * 100 : 0;
const cpuUsage = hostData.data.cpu * 100;
const memoryUsage = (hostData.data.memory.used / hostData.data.memory.total) * 100;
const failedTasks = tasksData.total >= 100 ? "99+" : tasksData.total;
+1 -1
View File
@@ -15,7 +15,7 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) {
const options = {
body: params
? Object.keys(params)
.map((prop) => `${prop}=${params[prop]}`)
.map((prop) => `${prop}=${encodeURIComponent(params[prop])}`)
.join("&")
: `session=${sessionId}`,
method: "POST",
+4 -1
View File
@@ -12,7 +12,10 @@ const widget = {
wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length,
have: jsonArrayFilter(data, (item) => item.hasFile).length,
missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length,
all: asJson(data),
all: asJson(data).map((entry) => ({
title: entry.title,
id: entry.id,
})),
}),
},
"queue/status": {
+29 -14
View File
@@ -4,31 +4,46 @@ import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
const ROMM_DEFAULT_FIELDS = ["platforms", "totalRoms", "saves", "states"];
const MAX_ALLOWED_FIELDS = 4;
export default function Component({ service }) {
const { widget } = service;
const { t } = useTranslation();
const { data: response, error: responseError } = useWidgetAPI(widget, "statistics");
if (responseError) {
return (
<Container service={service}>
<Block label="Error" value={responseError.message} />
</Container>
);
}
if (responseError) {
return <Container service={service} error={responseError} />;
}
if (response) {
const platforms = response.filter((x) => x.rom_count !== 0).length;
const totalRoms = response.reduce((total, stat) => total + stat.rom_count, 0);
if (!widget.fields?.length > 0) {
widget.fields = ROMM_DEFAULT_FIELDS;
} else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (!response) {
return (
<Container service={service}>
<Block label="romm.platforms" value={t("common.number", { value: platforms })} />
<Block label="romm.totalRoms" value={t("common.number", { value: totalRoms })} />
<Block label="romm.platforms" />
<Block label="romm.totalRoms" />
<Block label="romm.saves" />
<Block label="romm.states" />
<Block label="romm.screenshots" />
<Block label="romm.totalfilesize" />
</Container>
);
}
if (response) {
return (
<Container service={service}>
<Block label="romm.platforms" value={t("common.number", { value: response.PLATFORMS })} />
<Block label="romm.totalRoms" value={t("common.number", { value: response.ROMS })} />
<Block label="romm.saves" value={t("common.number", { value: response.SAVES })} />
<Block label="romm.states" value={t("common.number", { value: response.STATES })} />
<Block label="romm.screenshots" value={t("common.number", { value: response.SCREENSHOTS })} />
<Block label="romm.totalfilesize" value={t("common.bytes", { value: response.FILESIZE })} />
</Container>
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ const widget = {
mappings: {
statistics: {
endpoint: "platforms",
endpoint: "stats",
},
},
};
+63
View File
@@ -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>
);
}
+14
View File
@@ -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;
+2 -2
View File
@@ -46,12 +46,12 @@ export default function Component({ service }) {
<Block label="stash.scenes" value={t("common.number", { value: stats.scene_count })} />
<Block label="stash.scenesPlayed" value={t("common.number", { value: stats.scenes_played })} />
<Block label="stash.playCount" value={t("common.number", { value: stats.total_play_count })} />
<Block label="stash.playDuration" value={t("common.uptime", { value: stats.total_play_duration })} />
<Block label="stash.playDuration" value={t("common.duration", { value: stats.total_play_duration })} />
<Block
label="stash.sceneSize"
value={t("common.bbytes", { value: stats.scenes_size, maximumFractionDigits: 1 })}
/>
<Block label="stash.sceneDuration" value={t("common.uptime", { value: stats.scenes_duration })} />
<Block label="stash.sceneDuration" value={t("common.duration", { value: stats.scenes_duration })} />
<Block label="stash.images" value={t("common.number", { value: stats.image_count })} />
<Block
+110
View File
@@ -0,0 +1,110 @@
import { useTranslation } from "next-i18next";
import classNames from "classnames";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
function MarketStatus({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data, error } = useWidgetAPI(widget, "status", {
exchange: "US",
});
if (error || data?.error) {
return <Container service={service} error={error} />;
}
if (!data) {
return (
<Container service={service}>
<Block value={t("stocks.loading")} />
</Container>
);
}
const { isOpen } = data;
if (isOpen) {
return (
<span className="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-400/90 ring-1 ring-inset ring-green-500/20">
{t("stocks.open")}
</span>
);
}
return (
<span className="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400/60 ring-1 ring-inset ring-red-400/10">
{t("stocks.closed")}
</span>
);
}
function StockItem({ service, ticker }) {
const { t } = useTranslation();
const { widget } = service;
const { data, error } = useWidgetAPI(widget, "quote", { symbol: ticker });
if (error || data?.error) {
return <Container service={service} error={error} />;
}
if (!data) {
return (
<Container service={service}>
<Block value={t("stocks.loading")} />
</Container>
);
}
return (
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded flex flex-1 items-center justify-between m-1 p-1 text-xs">
<span className="font-thin ml-2 flex-none">{ticker}</span>
<div className="flex items-center flex-row-reverse mr-2 text-right">
<span className={`font-bold ml-2 w-10 ${data.dp > 0 ? "text-emerald-300" : "text-rose-300"}`}>
{data.dp?.toFixed(2) ? `${data.dp?.toFixed(2)}%` : t("widget.api_error")}
</span>
<span className="font-bold">
{data.c
? t("common.number", {
value: data?.c,
style: "currency",
currency: "USD",
})
: t("widget.api_error")}
</span>
</div>
</div>
);
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { watchlist, showUSMarketStatus } = widget;
if (!watchlist || !watchlist.length || watchlist.length > 28 || new Set(watchlist).size !== watchlist.length) {
return (
<Container service={service}>
<Block value={t("stocks.invalidConfiguration")} />
</Container>
);
}
return (
<Container service={service}>
<div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1 z-20")}>
{showUSMarketStatus === true && <MarketStatus service={service} />}
</div>
<div className="flex flex-col w-full">
{watchlist.map((ticker) => (
<StockItem key={ticker} service={service} ticker={ticker} />
))}
</div>
</Container>
);
}
+21
View File
@@ -0,0 +1,21 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: `https://finnhub.io/api/{endpoint}`,
proxyHandler: credentialedProxyHandler,
mappings: {
quote: {
// https://finnhub.io/docs/api/quote
endpoint: "v1/quote",
params: ["symbol"],
},
status: {
// https://finnhub.io/docs/api/market-status
endpoint: "v1/stock/market-status",
params: ["exchange"],
},
},
};
export default widget;
+40
View File
@@ -0,0 +1,40 @@
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: suwayomiData, error: suwayomiError } = useWidgetAPI(widget);
if (suwayomiError) {
return <Container service={service} error={suwayomiError} />;
}
if (!suwayomiData) {
if (!widget.fields || widget.fields.length === 0) {
widget.fields = ["download", "nondownload", "read", "unread"];
} else if (widget.fields.length > 4) {
widget.fields = widget.fields.slice(0, 4);
}
return (
<Container service={service}>
{widget.fields.map((field) => (
<Block key={field} label={`suwayomi.${field}`} />
))}
</Container>
);
}
return (
<Container service={service}>
{suwayomiData.map((data) => (
<Block key={data.label} label={data.label} value={t("common.number", { value: data.count })} />
))}
</Container>
);
}
+175
View File
@@ -0,0 +1,175 @@
import { httpProxy } from "utils/proxy/http";
import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const proxyName = "suwayomiProxyHandler";
const logger = createLogger(proxyName);
const countsToExtract = {
download: {
condition: (c) => c.isDownloaded,
gqlCondition: "isDownloaded: true",
},
nondownload: {
condition: (c) => !c.isDownloaded,
gqlCondition: "isDownloaded: false",
},
read: {
condition: (c) => c.isRead,
gqlCondition: "isRead: true",
},
unread: {
condition: (c) => !c.isRead,
gqlCondition: "isRead: false",
},
downloadedread: {
condition: (c) => c.isDownloaded && c.isRead,
gqlCondition: "isDownloaded: true, isRead: true",
},
downloadedunread: {
condition: (c) => c.isDownloaded && !c.isRead,
gqlCondition: "isDownloaded: true, isRead: false",
},
nondownloadedread: {
condition: (c) => !c.isDownloaded && c.isRead,
gqlCondition: "isDownloaded: false, isRead: true",
},
nondownloadedunread: {
condition: (c) => !c.isDownloaded && !c.isRead,
gqlCondition: "isDownloaded: false, isRead: false",
},
};
function makeBody(fields, category = "all") {
if (Number.isNaN(Number(category))) {
let query = "";
fields.forEach((field) => {
query += `
${field}: chapters(
condition: {${countsToExtract[field].gqlCondition}}
filter: {inLibrary: {equalTo: true}}
) {
totalCount
}`;
});
return JSON.stringify({
operationName: "Counts",
query: `
query Counts {
${query}
}`,
});
}
return JSON.stringify({
operationName: "category",
query: `
query category($id: Int!) {
category(id: $id) {
# name
mangas {
nodes {
chapters {
nodes {
isRead
isDownloaded
}
}
}
}
}
}`,
variables: {
id: Number(category),
},
});
}
function extractCounts(responseJSON, fields) {
if (!("category" in responseJSON.data)) {
return fields.map((field) => ({
count: responseJSON.data[field].totalCount,
label: `suwayomi.${field}`,
}));
}
const tmp = responseJSON.data.category.mangas.nodes.reduce(
(accumulator, manga) => {
manga.chapters.nodes.forEach((chapter) => {
fields.forEach((field, i) => {
if (countsToExtract[field].condition(chapter)) {
accumulator[i] += 1;
}
});
});
return accumulator;
},
[0, 0, 0, 0],
);
return fields.map((field, i) => ({
count: tmp[i],
label: `suwayomi.${field}`,
}));
}
export default async function suwayomiProxyHandler(req, res) {
const { group, service, endpoint } = 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);
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" });
}
if (!widget.fields || widget.fields.length === 0) {
widget.fields = ["download", "nondownload", "read", "unread"];
} else if (widget.fields.length > 4) {
widget.fields = widget.fields.slice(0, 4);
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const body = makeBody(widget.fields, widget.category);
const headers = {
"Content-Type": "application/json",
};
if (widget.username && widget.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
}
const [status, contentType, data] = await httpProxy(url, {
method: "POST",
body,
headers,
});
if (status === 401) {
logger.error("Invalid or missing username or password for service '%s' in group '%s'", service, group);
return res.status(status).send({ error: { message: "401: unauthorized, username or password is incorrect." } });
}
if (status !== 200) {
logger.error(
"Error getting data from Suwayomi for service '%s' in group '%s': %d. Data: %s",
service,
group,
status,
data,
);
return res.status(status).send({ error: { message: "Error getting data. body: %s, data: %s", body, data } });
}
const returnData = extractCounts(JSON.parse(data), widget.fields);
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(returnData);
}
+8
View File
@@ -0,0 +1,8 @@
import suwayomiProxyHandler from "./proxy";
const widget = {
api: "{url}/api/graphql",
proxyHandler: suwayomiProxyHandler,
};
export default widget;
+1 -1
View File
@@ -205,7 +205,7 @@ export default function Component({ service }) {
<div className="flex flex-col pb-1 mx-1">
{playing.map((session) => (
<SessionEntry
key={session.Id}
key={session.session_key}
session={session}
enableUser={enableUser}
showEpisodeNumber={showEpisodeNumber}
+7 -5
View File
@@ -21,9 +21,13 @@ export default async function tdarrProxyHandler(req, res) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}
const headers = {
"content-type": "application/json",
};
if (widget.key) {
headers["x-api-key"] = `${widget.key}`;
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: undefined, ...widget }));
const [status, contentType, data] = await httpProxy(url, {
method: "POST",
body: JSON.stringify({
@@ -33,9 +37,7 @@ export default async function tdarrProxyHandler(req, res) {
docID: "statistics",
},
}),
headers: {
"content-type": "application/json",
},
headers,
});
if (status !== 200) {
+130
View File
@@ -0,0 +1,130 @@
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";
const MAX_ALLOWED_FIELDS = 4;
export const technitiumDefaultFields = ["totalQueries", "totalAuthoritative", "totalCached", "totalServerFailure"];
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const params = {
type: widget.range ?? "LastHour",
};
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats", params);
// Default fields
if (!widget.fields?.length > 0) {
widget.fields = technitiumDefaultFields;
}
// Limits max number of displayed fields
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (statsError) {
return <Container service={service} error={statsError} />;
}
if (!statsData) {
return (
<Container service={service}>
<Block label="technitium.totalQueries" />
<Block label="technitium.totalNoError" />
<Block label="technitium.totalServerFailure" />
<Block label="technitium.totalNxDomain" />
<Block label="technitium.totalRefused" />
<Block label="technitium.totalAuthoritative" />
<Block label="technitium.totalRecursive" />
<Block label="technitium.totalCached" />
<Block label="technitium.totalBlocked" />
<Block label="technitium.totalDropped" />
<Block label="technitium.totalClients" />
</Container>
);
}
function toPercent(value, total) {
return t("common.percent", {
value: !Number.isNaN(value / total) ? 100 * (value / total) : 0,
maximumFractionDigits: 2,
});
}
return (
<Container service={service}>
<Block label="technitium.totalQueries" value={`${t("common.number", { value: statsData.totalQueries })}`} />
<Block
label="technitium.totalNoError"
value={`${t("common.number", { value: statsData.totalNoError })} (${toPercent(
statsData.totalNoError,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalServerFailure"
value={`${t("common.number", { value: statsData.totalServerFailure })} (${toPercent(
statsData.totalServerFailure,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalNxDomain"
value={`${t("common.number", { value: statsData.totalNxDomain })} (${toPercent(
statsData.totalNxDomain,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalRefused"
value={`${t("common.number", { value: statsData.totalRefused })} (${toPercent(
statsData.totalRefused,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalAuthoritative"
value={`${t("common.number", { value: statsData.totalAuthoritative })} (${toPercent(
statsData.totalAuthoritative,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalRecursive"
value={`${t("common.number", { value: statsData.totalRecursive })} (${toPercent(
statsData.totalRecursive,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalCached"
value={`${t("common.number", { value: statsData.totalCached })} (${toPercent(
statsData.totalCached,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalBlocked"
value={`${t("common.number", { value: statsData.totalBlocked })} (${toPercent(
statsData.totalBlocked,
statsData.totalQueries,
)})`}
/>
<Block
label="technitium.totalDropped"
value={`${t("common.number", { value: statsData.totalDropped })} (${toPercent(
statsData.totalDropped,
statsData.totalQueries,
)})`}
/>
<Block label="technitium.totalClients" value={`${t("common.number", { value: statsData.totalClients })}`} />
</Container>
);
}
+17
View File
@@ -0,0 +1,17 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson } from "utils/proxy/api-helpers";
const widget = {
api: "{url}/api/{endpoint}?token={key}&utc=true",
proxyHandler: genericProxyHandler,
mappings: {
stats: {
endpoint: "dashboard/stats/get",
validate: ["response", "status"],
params: ["type"],
map: (data) => asJson(data).response?.stats,
},
},
};
export default widget;
+1 -1
View File
@@ -35,7 +35,7 @@ export default function Component({ service }) {
<>
<Container service={service}>
<Block label="truenas.load" value={t("common.number", { value: statusData.loadavg[0] })} />
<Block label="truenas.uptime" value={t("common.uptime", { value: statusData.uptime_seconds })} />
<Block label="truenas.uptime" value={t("common.duration", { value: statusData.uptime_seconds })} />
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
</Container>
{enablePools &&
+1 -1
View File
@@ -25,7 +25,7 @@ const widget = {
healthy: entry.healthy,
allocated: entry.allocated,
free: entry.free,
data: entry.topology.data,
data: entry.topology?.data ?? [],
})),
},
},
+4 -10
View File
@@ -32,16 +32,10 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block
label="tubearchivist.downloads"
value={t("common.number", { value: downloadsData?.paginate?.total_hits })}
/>
<Block label="tubearchivist.videos" value={t("common.number", { value: videosData?.paginate?.total_hits })} />
<Block label="tubearchivist.channels" value={t("common.number", { value: channelsData?.paginate?.total_hits })} />
<Block
label="tubearchivist.playlists"
value={t("common.number", { value: playlistsData?.paginate?.total_hits })}
/>
<Block label="tubearchivist.downloads" value={t("common.number", { value: downloadsData.pending ?? 0 })} />
<Block label="tubearchivist.videos" value={t("common.number", { value: videosData.doc_count ?? 0 })} />
<Block label="tubearchivist.channels" value={t("common.number", { value: channelsData.doc_count ?? 0 })} />
<Block label="tubearchivist.playlists" value={t("common.number", { value: playlistsData.doc_count ?? 0 })} />
</Container>
);
}
+7 -8
View File
@@ -6,20 +6,19 @@ const widget = {
mappings: {
downloads: {
endpoint: "download",
validate: ["paginate"],
endpoint: "stats/download",
},
videos: {
endpoint: "video",
validate: ["paginate"],
endpoint: "stats/video",
validate: ["doc_count"],
},
channels: {
endpoint: "channel",
validate: ["paginate"],
endpoint: "stats/channel",
validate: ["doc_count"],
},
playlists: {
endpoint: "playlist",
validate: ["paginate"],
endpoint: "stats/playlist",
validate: ["doc_count"],
},
},
};
-1
View File
@@ -1,4 +1,3 @@
// import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
+2 -2
View File
@@ -58,7 +58,7 @@ export default function Component({ service }) {
break;
case 2:
status = t("uptimerobot.up");
uptime = t("common.uptime", { value: monitor.logs[0].duration });
uptime = t("common.duration", { value: monitor.logs[0].duration });
logIndex = 1;
break;
case 8:
@@ -73,7 +73,7 @@ export default function Component({ service }) {
}
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
const downDuration = t("common.uptime", { value: monitor.logs[logIndex].duration });
const downDuration = t("common.duration", { value: monitor.logs[logIndex].duration });
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
return (
+68
View File
@@ -0,0 +1,68 @@
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: projectsData, error: projectsError } = useWidgetAPI(widget, "projects");
const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "tasks");
if (projectsError || tasksError) {
return <Container service={service} error={projectsError ?? tasksError} />;
}
if (!projectsData || !tasksData) {
return (
<Container service={service}>
<Block label="vikunja.projects" />
<Block label="vikunja.tasks7d" />
<Block label="vikunja.tasksOverdue" />
<Block label="vikunja.tasksInProgress" />
</Container>
);
}
const projects = projectsData.filter((project) => project.id > 0); // saved filters have id < 0
const oneWeekFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const tasksWithDueDate = tasksData.filter((task) => !task.dueDateIsDefault);
const tasks7d = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= oneWeekFromNow);
const tasksOverdue = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= new Date(Date.now()));
const tasksInProgress = tasksData.filter((task) => task.inProgress);
return (
<>
<Container service={service}>
<Block label="vikunja.projects" value={t("common.number", { value: projects.length })} />
<Block label="vikunja.tasks7d" value={t("common.number", { value: tasks7d.length })} />
<Block label="vikunja.tasksOverdue" value={t("common.number", { value: tasksOverdue.length })} />
<Block label="vikunja.tasksInProgress" value={t("common.number", { value: tasksInProgress.length })} />
</Container>
{widget.enableTaskList &&
tasksData.slice(0, 5).map((task) => (
<div
key={task.id}
className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex"
>
<div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
<div className="absolute w-full h-4 whitespace-nowrap text-ellipsis overflow-hidden text-left">
{task.title}
</div>
</div>
{!task.dueDateIsDefault && (
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
{t("common.relativeDate", {
value: task.dueDate,
formatParams: { value: { style: "narrow", numeric: "auto" } },
})}
</div>
)}
</div>
))}
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import { asJson } from "utils/proxy/api-helpers";
const widget = {
api: `{url}/api/v1/{endpoint}`,
proxyHandler: credentialedProxyHandler,
mappings: {
projects: {
endpoint: "projects",
},
tasks: {
endpoint: "tasks/all?filter=done%3Dfalse&sort_by=due_date",
map: (data) =>
asJson(data).map((task) => ({
id: task.id,
title: task.title,
priority: task.priority,
dueDate: task.due_date,
dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z",
inProgress: task.percent_done > 0 && task.percent_done < 1,
})),
},
},
};
export default widget;
+4 -4
View File
@@ -5,14 +5,14 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { widget } = service;
const { data: infoData, error: infoError } = useWidgetAPI(widget);
const { data: infoData, error: infoError } = useWidgetAPI(widget, "client");
if (!widget.fields) {
widget.fields = ["connected", "enabled", "total"];
}
if (infoError) {
return <Container service={service} error={infoError} />;
if (infoError || infoData?.statusCode > 400) {
return <Container service={service} error={infoError ?? { message: infoData.statusMessage, data: infoData }} />;
}
if (!infoData) {
@@ -38,7 +38,7 @@ export default function Component({ service }) {
<Container service={service}>
<Block label="wgeasy.connected" value={connected} />
<Block label="wgeasy.enabled" value={enabled} />
<Block label="wgeasy.diabled" value={disabled} />
<Block label="wgeasy.disabled" value={disabled} />
<Block label="wgeasy.total" value={infoData.length} />
</Container>
);
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff. Show more