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. 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. 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 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 ## 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. 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. description: Watch movies and TV shows.
server: localhost server: localhost
container: plex container: plex
widget: widgets:
- Tautulli:
type: tautulli type: tautulli
url: http://172.16.1.1:8181 url: http://172.16.1.1:8181
key: aabbccddeeffgghhiijjkkllmmnnoo key: aabbccddeeffgghhiijjkkllmmnnoo
- UptimeKuma:
type: uptimekuma
url: http://172.16.1.2:8080
slug: aaaaaaabbbbb
``` ```
## Info Widgets ## Info Widgets

View File

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

View File

@ -3,22 +3,24 @@ import { useTranslation } from "next-i18next";
import ErrorBoundary from "components/errorboundry"; import ErrorBoundary from "components/errorboundry";
import components from "widgets/components"; import components from "widgets/components";
export default function Widget({ service }) { export default function Widget({ widget, service }) {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const ServiceWidget = components[service.widget.type]; const ServiceWidget = components[widget.type];
const fullService = service;
fullService.widget = widget;
if (ServiceWidget) { if (ServiceWidget) {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<ServiceWidget service={service} /> <ServiceWidget service={fullService} />
</ErrorBoundary> </ErrorBoundary>
); );
} }
return ( 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="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> </div>
); );
} }

View File

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

View File

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

View File

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

View File

@ -9,10 +9,10 @@ import widgets from "widgets/widgets";
const logger = createLogger("credentialedProxyHandler"); const logger = createLogger("credentialedProxyHandler");
export default async function credentialedProxyHandler(req, res, map) { export default async function credentialedProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query; const { group, service, endpoint, name } = req.query;
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service); const widget = await getServiceWidget(group, service, name ? name : null);
if (!widgets?.[widget.type]?.api) { if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" }); 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"); const logger = createLogger("genericProxyHandler");
export default async function genericProxyHandler(req, res, map) { export default async function genericProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query; const { group, service, endpoint, name } = req.query;
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service); const widget = await getServiceWidget(group, service, name ? name : null);
if (!widgets?.[widget.type]?.api) { if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" }); 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) { 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) { 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 api = widgets?.[widget.type]?.api;
const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method); 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) { export default async function synologyProxyHandler(req, res) {
const { group, service, endpoint } = req.query; const { group, service, endpoint, name } = req.query;
if (!group || !service) { if (!group || !service) {
return res.status(400).json({ error: "Invalid proxy service type" }); 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 widget = widgets?.[serviceWidget.type];
const mapping = widget?.mappings?.[endpoint]; const mapping = widget?.mappings?.[endpoint];
if (!widget.api || !mapping) { if (!widget.api || !mapping) {