Merge branch 'gethomepage:dev' into integration
This commit is contained in:
commit
9326155ab8
@ -3,10 +3,12 @@ title: Beszel
|
|||||||
description: Beszel Widget Configuration
|
description: Beszel Widget Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
Learn more about [Beszel]()
|
Learn more about [Beszel](https://github.com/henrygd/beszel)
|
||||||
|
|
||||||
The widget has two modes, a single system with detailed info if `systemId` is provided, or an overview of all systems if `systemId` is not provided.
|
The widget has two modes, a single system with detailed info if `systemId` is provided, or an overview of all systems if `systemId` is not provided.
|
||||||
|
|
||||||
|
The `systemID` in the `id` field on the collections page of Beszel.
|
||||||
|
|
||||||
Allowed fields for 'overview' mode: `["systems", "up"]`
|
Allowed fields for 'overview' mode: `["systems", "up"]`
|
||||||
Allowed fields for a single system: `["name", "status", "updated", "cpu", "memory", "disk", "network"]`
|
Allowed fields for a single system: `["name", "status", "updated", "cpu", "memory", "disk", "network"]`
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
|||||||
- [Plex](plex.md)
|
- [Plex](plex.md)
|
||||||
- [Portainer](portainer.md)
|
- [Portainer](portainer.md)
|
||||||
- [Prometheus](prometheus.md)
|
- [Prometheus](prometheus.md)
|
||||||
|
- [Prometheus Metric](prometheusmetric.md)
|
||||||
- [Prowlarr](prowlarr.md)
|
- [Prowlarr](prowlarr.md)
|
||||||
- [Proxmox](proxmox.md)
|
- [Proxmox](proxmox.md)
|
||||||
- [Proxmox Backup Server](proxmoxbackupserver.md)
|
- [Proxmox Backup Server](proxmoxbackupserver.md)
|
||||||
|
|||||||
67
docs/widgets/services/prometheusmetric.md
Normal file
67
docs/widgets/services/prometheusmetric.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Prometheus Metric
|
||||||
|
description: Prometheus Metric Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Querying Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/).
|
||||||
|
|
||||||
|
This widget can show metrics for your service defined by PromQL queries which are requested from a running Prometheus instance.
|
||||||
|
|
||||||
|
Quries can be defined in the `metrics` array of the widget along with a label to be used to present the metric value. You can optionally specify a global `refreshInterval` in milliseconds and/or define the `refreshInterval` per metric. Inside the optional `format` object of a metric various formatting styles and transformations can be applied (see below).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: prometheusmetric
|
||||||
|
url: https://prometheus.host.or.ip
|
||||||
|
refreshInterval: 10000 # optional - in milliseconds, defaults to 10s
|
||||||
|
metrics:
|
||||||
|
- label: Metric 1
|
||||||
|
query: alertmanager_alerts{state="active"}
|
||||||
|
- label: Metric 2
|
||||||
|
query: apiserver_storage_size_bytes{node="mynode"}
|
||||||
|
format:
|
||||||
|
type: bytes
|
||||||
|
- label: Metric 3
|
||||||
|
query: avg(prometheus_notifications_latency_seconds)
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
suffix: s
|
||||||
|
options:
|
||||||
|
maximumFractionDigits: 4
|
||||||
|
- label: Metric 4
|
||||||
|
query: time()
|
||||||
|
refreshInterval: 1000 # will override global refreshInterval
|
||||||
|
format:
|
||||||
|
type: date
|
||||||
|
scale: 1000
|
||||||
|
options:
|
||||||
|
timeStyle: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
Supported values for `format.type` are `text`, `number`, `percent`, `bytes`, `bits`, `bbytes`, `bbits`, `byterate`, `bibyterate`, `bitrate`, `bibitrate`, `date`, `duration`, `relativeDate`, and `text` which is the default.
|
||||||
|
|
||||||
|
The `dateStyle` and `timeStyle` options of the `date` format are passed directly to [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) and the `style` and `numeric` options of `relativeDate` are passed to [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat). For the `number` format, options of [Intl.NumberFormat](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) can be used, e.g. `maximumFractionDigits` or `minimumFractionDigits`.
|
||||||
|
|
||||||
|
### Data Transformation
|
||||||
|
|
||||||
|
You can manipulate your metric value with the following tools: `scale`, `prefix` and `suffix`, for example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 1
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
scale: 1000 # multiplies value by a number or fraction string e.g. 1/16
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 2
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
prefix: "$" # prefixes value with given string
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 3
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
suffix: "€" # suffixes value with given string
|
||||||
|
```
|
||||||
20
docs/widgets/services/suwayomi.md
Normal file
20
docs/widgets/services/suwayomi.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Suwayomi
|
||||||
|
description: Suwayomi Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Suwayomi](https://github.com/Suwayomi/Suwayomi-Server).
|
||||||
|
|
||||||
|
Allowed fields: ["download", "nondownload", "read", "unread", "downloadedread", "downloadedunread", "nondownloadedread", "nondownloadedunread"]
|
||||||
|
|
||||||
|
The widget defaults to the first four above. If more than four fields are provided, only the first 4 are displayed.
|
||||||
|
Category IDs can be obtained from the url when navigating to it, `?tab={categoryID}`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: suwayomi
|
||||||
|
url: http://suwayomi.host.or.ip
|
||||||
|
username: username #optional
|
||||||
|
password: password #optional
|
||||||
|
category: 0 #optional, defaults to all categories
|
||||||
|
```
|
||||||
@ -121,6 +121,7 @@ nav:
|
|||||||
- widgets/services/plex.md
|
- widgets/services/plex.md
|
||||||
- widgets/services/portainer.md
|
- widgets/services/portainer.md
|
||||||
- widgets/services/prometheus.md
|
- widgets/services/prometheus.md
|
||||||
|
- widgets/services/prometheusmetric.md
|
||||||
- widgets/services/prowlarr.md
|
- widgets/services/prowlarr.md
|
||||||
- widgets/services/proxmox.md
|
- widgets/services/proxmox.md
|
||||||
- widgets/services/proxmoxbackupserver.md
|
- widgets/services/proxmoxbackupserver.md
|
||||||
|
|||||||
@ -309,6 +309,16 @@
|
|||||||
"stopped": "Stopped",
|
"stopped": "Stopped",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
|
"suwayomi": {
|
||||||
|
"download": "Downloaded",
|
||||||
|
"nondownload": "Non-Downloaded",
|
||||||
|
"read": "Read",
|
||||||
|
"unread": "Unread",
|
||||||
|
"downloadedread": "Downloaded & Read",
|
||||||
|
"downloadedunread": "Downloaded & Unread",
|
||||||
|
"nondownloadedread": "Non-Downloaded & Read",
|
||||||
|
"nondownloadedunread": "Non-Downloaded & Unread"
|
||||||
|
},
|
||||||
"tailscale": {
|
"tailscale": {
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
|
|||||||
@ -332,7 +332,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
pointsLimit,
|
pointsLimit,
|
||||||
diskUnits,
|
diskUnits,
|
||||||
|
|
||||||
// glances, customapi, iframe
|
// glances, customapi, iframe, prometheusmetric
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
|
||||||
// hdhomerun
|
// hdhomerun
|
||||||
@ -375,6 +375,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
// opnsense, pfsense
|
// opnsense, pfsense
|
||||||
wan,
|
wan,
|
||||||
|
|
||||||
|
// prometheusmetric
|
||||||
|
metrics,
|
||||||
|
|
||||||
// proxmox
|
// proxmox
|
||||||
node,
|
node,
|
||||||
|
|
||||||
@ -560,6 +563,10 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (type === "vikunja") {
|
if (type === "vikunja") {
|
||||||
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
|
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
|
||||||
}
|
}
|
||||||
|
if (type === "prometheusmetric") {
|
||||||
|
if (metrics) cleanedService.widget.metrics = metrics;
|
||||||
|
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
|||||||
@ -95,6 +95,7 @@ const components = {
|
|||||||
plex: dynamic(() => import("./plex/component")),
|
plex: dynamic(() => import("./plex/component")),
|
||||||
portainer: dynamic(() => import("./portainer/component")),
|
portainer: dynamic(() => import("./portainer/component")),
|
||||||
prometheus: dynamic(() => import("./prometheus/component")),
|
prometheus: dynamic(() => import("./prometheus/component")),
|
||||||
|
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
|
||||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||||
proxmox: dynamic(() => import("./proxmox/component")),
|
proxmox: dynamic(() => import("./proxmox/component")),
|
||||||
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
||||||
@ -113,6 +114,7 @@ const components = {
|
|||||||
stocks: dynamic(() => import("./stocks/component")),
|
stocks: dynamic(() => import("./stocks/component")),
|
||||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||||
|
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||||
tailscale: dynamic(() => import("./tailscale/component")),
|
tailscale: dynamic(() => import("./tailscale/component")),
|
||||||
tandoor: dynamic(() => import("./tandoor/component")),
|
tandoor: dynamic(() => import("./tandoor/component")),
|
||||||
tautulli: dynamic(() => import("./tautulli/component")),
|
tautulli: dynamic(() => import("./tautulli/component")),
|
||||||
|
|||||||
115
src/widgets/prometheusmetric/component.jsx
Normal file
115
src/widgets/prometheusmetric/component.jsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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 (!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
src/widgets/prometheusmetric/widget.js
Normal file
16
src/widgets/prometheusmetric/widget.js
Normal 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;
|
||||||
40
src/widgets/suwayomi/component.jsx
Normal file
40
src/widgets/suwayomi/component.jsx
Normal 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
src/widgets/suwayomi/proxy.js
Normal file
175
src/widgets/suwayomi/proxy.js
Normal 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
src/widgets/suwayomi/widget.js
Normal file
8
src/widgets/suwayomi/widget.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import suwayomiProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/graphql",
|
||||||
|
proxyHandler: suwayomiProxyHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@ -205,7 +205,7 @@ export default function Component({ service }) {
|
|||||||
<div className="flex flex-col pb-1 mx-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
{playing.map((session) => (
|
{playing.map((session) => (
|
||||||
<SessionEntry
|
<SessionEntry
|
||||||
key={session.Id}
|
key={session.session_key}
|
||||||
session={session}
|
session={session}
|
||||||
enableUser={enableUser}
|
enableUser={enableUser}
|
||||||
showEpisodeNumber={showEpisodeNumber}
|
showEpisodeNumber={showEpisodeNumber}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ import plantit from "./plantit/widget";
|
|||||||
import plex from "./plex/widget";
|
import plex from "./plex/widget";
|
||||||
import portainer from "./portainer/widget";
|
import portainer from "./portainer/widget";
|
||||||
import prometheus from "./prometheus/widget";
|
import prometheus from "./prometheus/widget";
|
||||||
|
import prometheusmetric from "./prometheusmetric/widget";
|
||||||
import prowlarr from "./prowlarr/widget";
|
import prowlarr from "./prowlarr/widget";
|
||||||
import proxmox from "./proxmox/widget";
|
import proxmox from "./proxmox/widget";
|
||||||
import pterodactyl from "./pterodactyl/widget";
|
import pterodactyl from "./pterodactyl/widget";
|
||||||
@ -104,6 +105,7 @@ import stash from "./stash/widget";
|
|||||||
import stocks from "./stocks/widget";
|
import stocks from "./stocks/widget";
|
||||||
import strelaysrv from "./strelaysrv/widget";
|
import strelaysrv from "./strelaysrv/widget";
|
||||||
import swagdashboard from "./swagdashboard/widget";
|
import swagdashboard from "./swagdashboard/widget";
|
||||||
|
import suwayomi from "./suwayomi/widget";
|
||||||
import tailscale from "./tailscale/widget";
|
import tailscale from "./tailscale/widget";
|
||||||
import tandoor from "./tandoor/widget";
|
import tandoor from "./tandoor/widget";
|
||||||
import tautulli from "./tautulli/widget";
|
import tautulli from "./tautulli/widget";
|
||||||
@ -218,6 +220,7 @@ const widgets = {
|
|||||||
plex,
|
plex,
|
||||||
portainer,
|
portainer,
|
||||||
prometheus,
|
prometheus,
|
||||||
|
prometheusmetric,
|
||||||
prowlarr,
|
prowlarr,
|
||||||
proxmox,
|
proxmox,
|
||||||
pterodactyl,
|
pterodactyl,
|
||||||
@ -236,6 +239,7 @@ const widgets = {
|
|||||||
stocks,
|
stocks,
|
||||||
strelaysrv,
|
strelaysrv,
|
||||||
swagdashboard,
|
swagdashboard,
|
||||||
|
suwayomi,
|
||||||
tailscale,
|
tailscale,
|
||||||
tandoor,
|
tandoor,
|
||||||
tautulli,
|
tautulli,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user