Features: Add multiple widgets per service

This commit is contained in:
damii 2024-11-27 18:52:18 +11:00
parent 5ee2ea559c
commit 43b08ccd68
11 changed files with 327 additions and 288 deletions

View File

@ -5,7 +5,7 @@ description: Service Widget Configuration
Unless otherwise noted, URLs should not end with a `/` or other API path. Each widget will handle the path on its own.
Each service can have one widget attached to it (often matching the service type, but that's not forced).
Each service can have widgets attached to it (often matching the service type, but that's not forced).
In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details.
@ -22,6 +22,28 @@ Using Emby as an example, this is how you would attach the Emby service widget.
key: apikeyapikeyapikeyapikeyapikey
```
## Multiple Widgets
Each service can have multiple widgets attached to it. Each widget needs a unique name.
Using Emby as an example, here is how you would attack the Emby service widget alongside an uptime widget.
```yaml
- Emby:
icon: emby.png
href: http://emby.host.or.ip/
description: Movies & TV Shows
widgets:
- Emby:
type: emby
url: http://emby.host.or.ip
key: apikeyapikeyapikeyapikeyapikey
- Uptime:
type: uptimekuma
url: http://uptimekuma.host.or.ip:port
slug: statuspageslug
```
## Field Visibility
Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields.

View File

@ -19,10 +19,15 @@ Service widgets are used to display the status of a service, often a web service
description: Watch movies and TV shows.
server: localhost
container: plex
widget:
type: tautulli
url: http://172.16.1.1:8181
key: aabbccddeeffgghhiijjkkllmmnnoo
widgets:
- Tautulli:
type: tautulli
url: http://172.16.1.1:8181
key: aabbccddeeffgghhiijjkkllmmnnoo
- UptimeKuma:
type: uptimekuma
url: http://172.16.1.2:8080
slug: aaaaaaabbbbb
```
## Info Widgets

View File

@ -154,7 +154,9 @@ export default function Item({ service, group, useEqualHeights }) {
</div>
)}
{service.widget && <Widget service={service} />}
{service.widgets.map((widget) => (
<Widget widget={widget} service={service} />
))}
</div>
</li>
);

View File

@ -3,22 +3,24 @@ import { useTranslation } from "next-i18next";
import ErrorBoundary from "components/errorboundry";
import components from "widgets/components";
export default function Widget({ service }) {
export default function Widget({ widget, service }) {
const { t } = useTranslation("common");
const ServiceWidget = components[service.widget.type];
const ServiceWidget = components[widget.type];
const fullService = service;
fullService.widget = widget;
if (ServiceWidget) {
return (
<ErrorBoundary>
<ServiceWidget service={service} />
<ServiceWidget service={fullService} />
</ErrorBoundary>
);
}
return (
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1 service-missing">
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
<div className="font-thin text-sm">{t("widget.missing_type", { type: widget.type })}</div>
</div>
);
}

View File

@ -9,8 +9,8 @@ const logger = createLogger("servicesProxy");
export default async function handler(req, res) {
try {
const { service, group } = req.query;
const serviceWidget = await getServiceWidget(group, service);
const { service, group, name } = req.query;
const serviceWidget = await getServiceWidget(group, service, name);
let type = serviceWidget?.type;
// exceptions

View File

@ -354,311 +354,315 @@ export function cleanServiceGroups(groups) {
if (typeof cleanedService.weight !== "number") {
cleanedService.weight = 0;
}
cleanedService.widgets = cleanedService.widgets ? cleanedService.widgets : [];
if (cleanedService.widget != undefined) cleanedService.widgets.push(cleanedService.widget);
if (cleanedService.widgets != []) {
cleanedService.widgets = cleanedService.widgets.map((widget) => {
// whitelisted set of keys to pass to the frontend
// alphabetical, grouped by widget(s)
const {
// all widgets
fields,
hideErrors,
type,
if (cleanedService.widget) {
// whitelisted set of keys to pass to the frontend
// alphabetical, grouped by widget(s)
const {
// all widgets
fields,
hideErrors,
type,
// azuredevops
repositoryId,
userEmail,
// azuredevops
repositoryId,
userEmail,
// beszel
systemId,
// beszel
systemId,
// calendar
firstDayInWeek,
integrations,
maxEvents,
showTime,
previousDays,
view,
timezone,
// calendar
firstDayInWeek,
integrations,
maxEvents,
showTime,
previousDays,
view,
timezone,
// coinmarketcap
currency,
defaultinterval,
slugs,
symbols,
// coinmarketcap
currency,
defaultinterval,
slugs,
symbols,
// customapi
mappings,
display,
// customapi
mappings,
display,
// diskstation
volume,
// diskstation
volume,
// docker
container,
server,
// docker
container,
server,
// emby, jellyfin
enableBlocks,
enableNowPlaying,
// emby, jellyfin
enableBlocks,
enableNowPlaying,
// emby, jellyfin, tautulli
enableUser,
expandOneStreamToTwoRows,
showEpisodeNumber,
// emby, jellyfin, tautulli
enableUser,
expandOneStreamToTwoRows,
showEpisodeNumber,
// frigate
enableRecentEvents,
// frigate
enableRecentEvents,
// glances, immich, mealie, pihole, pfsense
version,
// glances, immich, mealie, pihole, pfsense
version,
// glances
chart,
metric,
pointsLimit,
diskUnits,
// glances
chart,
metric,
pointsLimit,
diskUnits,
// glances, customapi, iframe, prometheusmetric
refreshInterval,
// glances, customapi, iframe, prometheusmetric
refreshInterval,
// hdhomerun
tuner,
// hdhomerun
tuner,
// healthchecks
uuid,
// healthchecks
uuid,
// iframe
allowFullscreen,
allowPolicy,
allowScrolling,
classes,
loadingStrategy,
referrerPolicy,
src,
// iframe
allowFullscreen,
allowPolicy,
allowScrolling,
classes,
loadingStrategy,
referrerPolicy,
src,
// kopia
snapshotHost,
snapshotPath,
// kopia
snapshotHost,
snapshotPath,
// kubernetes
app,
namespace,
podSelector,
// kubernetes
app,
namespace,
podSelector,
// lubelogger
vehicleID,
// lubelogger
vehicleID,
// mjpeg
fit,
stream,
// mjpeg
fit,
stream,
// openmediavault
method,
// openmediavault
method,
// openwrt
interfaceName,
// openwrt
interfaceName,
// opnsense, pfsense
wan,
// opnsense, pfsense
wan,
// prometheusmetric
metrics,
// prometheusmetric
metrics,
// proxmox
node,
// proxmox
node,
// speedtest
bitratePrecision,
// speedtest
bitratePrecision,
// sonarr, radarr
enableQueue,
// sonarr, radarr
enableQueue,
// stocks
watchlist,
showUSMarketStatus,
// stocks
watchlist,
showUSMarketStatus,
// truenas
enablePools,
nasType,
// truenas
enablePools,
nasType,
// unifi
site,
// unifi
site,
// vikunja
enableTaskList,
// vikunja
enableTaskList,
// wgeasy
threshold,
// wgeasy
threshold,
// technitium
range,
// technitium
range,
// spoolman
spoolIds,
} = Object.keys(widget)[0] != "type" ? widget[Object.keys(widget)[0]] : widget;
// spoolman
spoolIds,
} = cleanedService.widget;
let fieldsList = fields;
if (typeof fields === "string") {
try {
fieldsList = JSON.parse(fields);
} catch (e) {
logger.error("Invalid fields list detected in config for service '%s'", service.name);
fieldsList = null;
let fieldsList = fields;
if (typeof fields === "string") {
try {
fieldsList = JSON.parse(fields);
} catch (e) {
logger.error("Invalid fields list detected in config for service '%s'", service.name);
fieldsList = null;
}
}
}
cleanedService.widget = {
type,
fields: fieldsList || null,
hide_errors: hideErrors || false,
service_name: service.name,
service_group: serviceGroup.name,
};
widget = {
type,
fields: fieldsList || null,
hide_errors: hideErrors || false,
service_name: service.name,
service_group: serviceGroup.name,
widget_name: Object.keys(widget)[0] != "type" ? Object.keys(widget)[0] : "",
};
if (type === "azuredevops") {
if (userEmail) cleanedService.widget.userEmail = userEmail;
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;
if (slugs) cleanedService.widget.slugs = slugs;
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
}
if (type === "docker") {
if (server) cleanedService.widget.server = server;
if (container) cleanedService.widget.container = container;
}
if (type === "unifi") {
if (site) cleanedService.widget.site = site;
}
if (type === "proxmox") {
if (node) cleanedService.widget.node = node;
}
if (type === "kubernetes") {
if (namespace) cleanedService.widget.namespace = namespace;
if (app) cleanedService.widget.app = app;
if (podSelector) cleanedService.widget.podSelector = podSelector;
}
if (type === "iframe") {
if (src) cleanedService.widget.src = src;
if (classes) cleanedService.widget.classes = classes;
if (referrerPolicy) cleanedService.widget.referrerPolicy = referrerPolicy;
if (allowPolicy) cleanedService.widget.allowPolicy = allowPolicy;
if (allowFullscreen) cleanedService.widget.allowFullscreen = allowFullscreen;
if (loadingStrategy) cleanedService.widget.loadingStrategy = loadingStrategy;
if (allowScrolling) cleanedService.widget.allowScrolling = allowScrolling;
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
}
if (["opnsense", "pfsense"].includes(type)) {
if (wan) cleanedService.widget.wan = wan;
}
if (["emby", "jellyfin"].includes(type)) {
if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks);
if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying);
}
if (["emby", "jellyfin", "tautulli"].includes(type)) {
if (expandOneStreamToTwoRows !== undefined)
cleanedService.widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows);
if (showEpisodeNumber !== undefined)
cleanedService.widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber);
if (enableUser !== undefined) cleanedService.widget.enableUser = !!JSON.parse(enableUser);
}
if (["sonarr", "radarr"].includes(type)) {
if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue);
}
if (type === "truenas") {
if (enablePools !== undefined) cleanedService.widget.enablePools = JSON.parse(enablePools);
if (nasType !== undefined) cleanedService.widget.nasType = nasType;
}
if (["diskstation", "qnap"].includes(type)) {
if (volume) cleanedService.widget.volume = volume;
}
if (type === "kopia") {
if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost;
if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath;
}
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;
if (chart !== undefined) {
cleanedService.widget.chart = chart;
} else {
cleanedService.widget.chart = true;
if (type === "azuredevops") {
if (userEmail) widget.userEmail = userEmail;
if (repositoryId) widget.repositoryId = repositoryId;
}
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit;
if (diskUnits) cleanedService.widget.diskUnits = diskUnits;
}
if (type === "mjpeg") {
if (stream) cleanedService.widget.stream = stream;
if (fit) cleanedService.widget.fit = fit;
}
if (type === "openmediavault") {
if (method) cleanedService.widget.method = method;
}
if (type === "openwrt") {
if (interfaceName) cleanedService.widget.interfaceName = interfaceName;
}
if (type === "customapi") {
if (mappings) cleanedService.widget.mappings = mappings;
if (display) cleanedService.widget.display = display;
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
}
if (type === "calendar") {
if (integrations) cleanedService.widget.integrations = integrations;
if (firstDayInWeek) cleanedService.widget.firstDayInWeek = firstDayInWeek;
if (view) cleanedService.widget.view = view;
if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
if (previousDays) cleanedService.widget.previousDays = previousDays;
if (showTime) cleanedService.widget.showTime = showTime;
if (timezone) cleanedService.widget.timezone = timezone;
}
if (type === "hdhomerun") {
if (tuner !== undefined) cleanedService.widget.tuner = tuner;
}
if (type === "healthchecks") {
if (uuid !== undefined) cleanedService.widget.uuid = uuid;
}
if (type === "speedtest") {
if (bitratePrecision !== undefined) {
cleanedService.widget.bitratePrecision = parseInt(bitratePrecision, 10);
if (type === "beszel") {
if (systemId) widget.systemId = systemId;
}
}
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;
}
if (type === "coinmarketcap") {
if (currency) widget.currency = currency;
if (symbols) widget.symbols = symbols;
if (slugs) widget.slugs = slugs;
if (defaultinterval) widget.defaultinterval = defaultinterval;
}
if (type === "docker") {
if (server) widget.server = server;
if (container) widget.container = container;
}
if (type === "unifi") {
if (site) widget.site = site;
}
if (type === "proxmox") {
if (node) widget.node = node;
}
if (type === "kubernetes") {
if (namespace) widget.namespace = namespace;
if (app) widget.app = app;
if (podSelector) widget.podSelector = podSelector;
}
if (type === "iframe") {
if (src) widget.src = src;
if (classes) widget.classes = classes;
if (referrerPolicy) widget.referrerPolicy = referrerPolicy;
if (allowPolicy) widget.allowPolicy = allowPolicy;
if (allowFullscreen) widget.allowFullscreen = allowFullscreen;
if (loadingStrategy) widget.loadingStrategy = loadingStrategy;
if (allowScrolling) widget.allowScrolling = allowScrolling;
if (refreshInterval) widget.refreshInterval = refreshInterval;
}
if (["opnsense", "pfsense"].includes(type)) {
if (wan) widget.wan = wan;
}
if (["emby", "jellyfin"].includes(type)) {
if (enableBlocks !== undefined) widget.enableBlocks = JSON.parse(enableBlocks);
if (enableNowPlaying !== undefined) widget.enableNowPlaying = JSON.parse(enableNowPlaying);
}
if (["emby", "jellyfin", "tautulli"].includes(type)) {
if (expandOneStreamToTwoRows !== undefined)
widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows);
if (showEpisodeNumber !== undefined) widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber);
if (enableUser !== undefined) widget.enableUser = !!JSON.parse(enableUser);
}
if (["sonarr", "radarr"].includes(type)) {
if (enableQueue !== undefined) widget.enableQueue = JSON.parse(enableQueue);
}
if (type === "truenas") {
if (enablePools !== undefined) widget.enablePools = JSON.parse(enablePools);
if (nasType !== undefined) widget.nasType = nasType;
}
if (["diskstation", "qnap"].includes(type)) {
if (volume) widget.volume = volume;
}
if (type === "kopia") {
if (snapshotHost) widget.snapshotHost = snapshotHost;
if (snapshotPath) widget.snapshotPath = snapshotPath;
}
if (["glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
if (version) widget.version = parseInt(version, 10);
}
if (type === "glances") {
if (metric) widget.metric = metric;
if (chart !== undefined) {
widget.chart = chart;
} else {
widget.chart = true;
}
if (refreshInterval) widget.refreshInterval = refreshInterval;
if (pointsLimit) widget.pointsLimit = pointsLimit;
if (diskUnits) widget.diskUnits = diskUnits;
}
if (type === "mjpeg") {
if (stream) widget.stream = stream;
if (fit) widget.fit = fit;
}
if (type === "openmediavault") {
if (method) widget.method = method;
}
if (type === "openwrt") {
if (interfaceName) widget.interfaceName = interfaceName;
}
if (type === "customapi") {
if (mappings) widget.mappings = mappings;
if (display) widget.display = display;
if (refreshInterval) widget.refreshInterval = refreshInterval;
}
if (type === "calendar") {
if (integrations) widget.integrations = integrations;
if (firstDayInWeek) widget.firstDayInWeek = firstDayInWeek;
if (view) widget.view = view;
if (maxEvents) widget.maxEvents = maxEvents;
if (previousDays) widget.previousDays = previousDays;
if (showTime) widget.showTime = showTime;
if (timezone) widget.timezone = timezone;
}
if (type === "hdhomerun") {
if (tuner !== undefined) widget.tuner = tuner;
}
if (type === "healthchecks") {
if (uuid !== undefined) widget.uuid = uuid;
}
if (type === "speedtest") {
if (bitratePrecision !== undefined) {
widget.bitratePrecision = parseInt(bitratePrecision, 10);
}
}
if (type === "stocks") {
if (watchlist) widget.watchlist = watchlist;
if (showUSMarketStatus) widget.showUSMarketStatus = showUSMarketStatus;
}
if (type === "wgeasy") {
if (threshold !== undefined) widget.threshold = parseInt(threshold, 10);
}
if (type === "frigate") {
if (enableRecentEvents !== undefined) widget.enableRecentEvents = enableRecentEvents;
}
if (type === "technitium") {
if (range !== undefined) widget.range = range;
}
if (type === "lubelogger") {
if (vehicleID !== undefined) widget.vehicleID = parseInt(vehicleID, 10);
}
if (type === "vikunja") {
if (enableTaskList !== undefined) widget.enableTaskList = !!enableTaskList;
}
if (type === "prometheusmetric") {
if (metrics) widget.metrics = metrics;
if (refreshInterval) widget.refreshInterval = refreshInterval;
}
if (type === "spoolman") {
if (spoolIds !== undefined) widget.spoolIds = spoolIds;
}
return widget;
});
}
return cleanedService;
@ -693,12 +697,15 @@ export async function getServiceItem(group, service) {
return false;
}
export default async function getServiceWidget(group, service) {
export default async function getServiceWidget(group, service, name = null) {
const serviceItem = await getServiceItem(group, service);
if (serviceItem) {
if (serviceItem && (name == null || name === "undefined")) {
const { widget } = serviceItem;
return widget;
}
if (serviceItem && name != null && name !== "undefined") {
const { widgets } = serviceItem;
return widgets.filter((widget) => Object.keys(widget)[0] == name)[0][name];
}
return false;
}

View File

@ -12,6 +12,7 @@ export function getURLSearchParams(widget, endpoint) {
const params = new URLSearchParams({
group: widget.service_group,
service: widget.service_name,
...(widget.widget_name != "" && { name: widget.widget_name }),
});
if (endpoint) {
params.append("endpoint", endpoint);

View File

@ -9,10 +9,10 @@ import widgets from "widgets/widgets";
const logger = createLogger("credentialedProxyHandler");
export default async function credentialedProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query;
const { group, service, endpoint, name } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
const widget = await getServiceWidget(group, service, name ? name : null);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });

View File

@ -8,10 +8,10 @@ import widgets from "widgets/widgets";
const logger = createLogger("genericProxyHandler");
export default async function genericProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query;
const { group, service, endpoint, name } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
const widget = await getServiceWidget(group, service, name ? name : null);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });

View File

@ -65,10 +65,10 @@ export async function sendJsonRpcRequest(url, method, params, widget) {
}
export default async function jsonrpcProxyHandler(req, res) {
const { group, service, endpoint: method } = req.query;
const { group, service, endpoint: method, name } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
const widget = await getServiceWidget(group, service, name ? name : null);
const api = widgets?.[widget.type]?.api;
const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method);

View File

@ -131,13 +131,13 @@ function toError(url, synologyError) {
}
export default async function synologyProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
const { group, service, endpoint, name } = req.query;
if (!group || !service) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
const serviceWidget = await getServiceWidget(group, service);
const serviceWidget = await getServiceWidget(group, service, name ? name : null);
const widget = widgets?.[serviceWidget.type];
const mapping = widget?.mappings?.[endpoint];
if (!widget.api || !mapping) {