diff --git a/docs/configs/service-widgets.md b/docs/configs/service-widgets.md index 9c54964e..8397e7c9 100644 --- a/docs/configs/service-widgets.md +++ b/docs/configs/service-widgets.md @@ -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. diff --git a/docs/widgets/index.md b/docs/widgets/index.md index 8b81ee40..faa30975 100644 --- a/docs/widgets/index.md +++ b/docs/widgets/index.md @@ -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 diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index a38dfaa3..09e74425 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -154,7 +154,9 @@ export default function Item({ service, group, useEqualHeights }) { )} - {service.widget && } + {service.widgets.map((widget) => ( + + ))} ); diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 292b2b1c..628ff8c2 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -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 ( - + ); } return (
-
{t("widget.missing_type", { type: service.widget.type })}
+
{t("widget.missing_type", { type: widget.type })}
); } diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index 90280c3d..e7439ced 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -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 diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index ea82c735..77e17aa7 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -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; } diff --git a/src/utils/proxy/api-helpers.js b/src/utils/proxy/api-helpers.js index 8e0682db..c762d0ba 100644 --- a/src/utils/proxy/api-helpers.js +++ b/src/utils/proxy/api-helpers.js @@ -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); diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index cbe0422a..416e38fc 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -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" }); diff --git a/src/utils/proxy/handlers/generic.js b/src/utils/proxy/handlers/generic.js index c6b9236b..7dc7fb53 100644 --- a/src/utils/proxy/handlers/generic.js +++ b/src/utils/proxy/handlers/generic.js @@ -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" }); diff --git a/src/utils/proxy/handlers/jsonrpc.js b/src/utils/proxy/handlers/jsonrpc.js index 3974dbdc..e95264bd 100644 --- a/src/utils/proxy/handlers/jsonrpc.js +++ b/src/utils/proxy/handlers/jsonrpc.js @@ -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); diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js index be44e810..e711db7e 100644 --- a/src/utils/proxy/handlers/synology.js +++ b/src/utils/proxy/handlers/synology.js @@ -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) {