From d581d70d1a81aedac041b53f4fd406d9ce1ab6e7 Mon Sep 17 00:00:00 2001 From: Matan Heimann Date: Thu, 24 Jul 2025 07:07:26 +0100 Subject: [PATCH] Feature: wallos service widget (#5562) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/wallos.md | 23 +++++++ public/locales/en/common.json | 7 +++ src/widgets/components.js | 1 + src/widgets/wallos/component.jsx | 100 +++++++++++++++++++++++++++++++ src/widgets/wallos/widget.js | 21 +++++++ src/widgets/widgets.js | 2 + 6 files changed, 154 insertions(+) create mode 100644 docs/widgets/services/wallos.md create mode 100644 src/widgets/wallos/component.jsx create mode 100644 src/widgets/wallos/widget.js diff --git a/docs/widgets/services/wallos.md b/docs/widgets/services/wallos.md new file mode 100644 index 000000000..f90cdb26b --- /dev/null +++ b/docs/widgets/services/wallos.md @@ -0,0 +1,23 @@ +--- +title: Wallos +description: Wallos Widget Configuration +--- + +Learn more about [Wallos](https://github.com/ellite/wallos). + +If you're using more than one currency to record subscriptions then you should also have your "Fixer API" key set-up (`Settings > Fixer API Key`). + +> **Please Note:** The monthly cost displayed is the total cost of subscriptions in that month, **not** the _"monthly"_ average cost. + +Get your API key under `Profile > API Key`. + +Allowed fields: `["activeSubscriptions", "nextRenewingSubscription", "previousMonthlyCost", "thisMonthlyCost", "nextMonthlyCost"]`. + +Default fields: `["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"]`. + +```yaml +widget: + type: wallos + url: http://wallos.host.or.ip + key: apikeyapikeyapikeyapikeyapikey +``` diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 07c12d5a8..a4bc77210 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1071,5 +1071,12 @@ "servers": "Servers", "stacks": "Stacks", "containers": "Containers" + }, + "wallos": { + "activeSubscriptions": "Subscriptions", + "thisMonthlyCost": "This Month", + "nextMonthlyCost": "Next Month", + "previousMonthlyCost": "Prev. Month", + "nextRenewingSubscription": "Next Payment" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index b652c2d86..b67ebc7ea 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -142,6 +142,7 @@ const components = { uptimerobot: dynamic(() => import("./uptimerobot/component")), urbackup: dynamic(() => import("./urbackup/component")), vikunja: dynamic(() => import("./vikunja/component")), + wallos: dynamic(() => import("./wallos/component")), watchtower: dynamic(() => import("./watchtower/component")), wgeasy: dynamic(() => import("./wgeasy/component")), whatsupdocker: dynamic(() => import("./whatsupdocker/component")), diff --git a/src/widgets/wallos/component.jsx b/src/widgets/wallos/component.jsx new file mode 100644 index 000000000..c3be5c434 --- /dev/null +++ b/src/widgets/wallos/component.jsx @@ -0,0 +1,100 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +const MAX_ALLOWED_FIELDS = 4; + +export default function Component({ service }) { + const todayDate = new Date(); + const { t } = useTranslation(); + const { widget } = service; + + if (!widget.fields) { + widget.fields = ["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"]; + } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS); + } + + const subscriptionsEndPoint = + widget.fields.includes("activeSubscriptions") || widget.fields.includes("nextRenewingSubscription") + ? "get_subscriptions" + : ""; + const { data: subscriptionsData, error: subscriptionsError } = useWidgetAPI(widget, subscriptionsEndPoint, { + state: 0, + sort: "next_payment", + }); + const subscriptionsThisMonthlyEndpoint = widget.fields.includes("thisMonthlyCost") ? "get_monthly_cost" : ""; + const { data: subscriptionsThisMonthlyCostData, error: subscriptionsThisMonthlyCostError } = useWidgetAPI( + widget, + subscriptionsThisMonthlyEndpoint, + { + month: todayDate.getMonth(), + year: todayDate.getFullYear(), + }, + ); + const subscriptionsNextMonthlyEndpoint = widget.fields.includes("nextMonthlyCost") ? "get_monthly_cost" : ""; + const { data: subscriptionsNextMonthlyCostData, error: subscriptionsNextMonthlyCostError } = useWidgetAPI( + widget, + subscriptionsNextMonthlyEndpoint, + { + month: todayDate.getMonth() + 1, + year: todayDate.getFullYear(), + }, + ); + const subscriptionsPreviousMonthlyEndpoint = widget.fields.includes("previousMonthlyCost") ? "get_monthly_cost" : ""; + const { data: subscriptionsPreviousMonthlyCostData, error: subscriptionsPreviousMonthlyCostError } = useWidgetAPI( + widget, + subscriptionsPreviousMonthlyEndpoint, + { + month: todayDate.getMonth() - 1, + year: todayDate.getFullYear(), + }, + ); + + if ( + subscriptionsError || + subscriptionsThisMonthlyCostError || + subscriptionsNextMonthlyCostError || + subscriptionsPreviousMonthlyCostError + ) { + const finalError = + subscriptionsError ?? + subscriptionsThisMonthlyCostError ?? + subscriptionsNextMonthlyCostError ?? + subscriptionsPreviousMonthlyCostError; + return ; + } + + if ( + (!subscriptionsData && + (widget.fields.includes("activeSubscriptions") || widget.fields.includes("nextRenewingSubscription"))) || + (!subscriptionsThisMonthlyCostData && widget.fields.includes("thisMonthlyCost")) || + (!subscriptionsNextMonthlyCostData && widget.fields.includes("nextMonthlyCost")) || + (!subscriptionsPreviousMonthlyCostData && widget.fields.includes("previousMonthlyCost")) + ) { + return ( + + + + + + + + ); + } + + return ( + + + + + + + + ); +} diff --git a/src/widgets/wallos/widget.js b/src/widgets/wallos/widget.js new file mode 100644 index 000000000..5eddbbc55 --- /dev/null +++ b/src/widgets/wallos/widget.js @@ -0,0 +1,21 @@ +import genericProxyHandler from "utils/proxy/handlers/generic"; + +const widget = { + api: "{url}/api/{endpoint}?api_key={key}", + proxyHandler: genericProxyHandler, + + mappings: { + get_monthly_cost: { + endpoint: "subscriptions/get_monthly_cost.php", + validate: ["localized_monthly_cost", "currency_symbol"], + params: ["month", "year"], + }, + get_subscriptions: { + endpoint: "subscriptions/get_subscriptions.php", + validate: ["subscriptions"], + params: ["state", "sort"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 8178f26ba..c1865fce3 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -133,6 +133,7 @@ import uptimekuma from "./uptimekuma/widget"; import uptimerobot from "./uptimerobot/widget"; import urbackup from "./urbackup/widget"; import vikunja from "./vikunja/widget"; +import wallos from "./wallos/widget"; import watchtower from "./watchtower/widget"; import wgeasy from "./wgeasy/widget"; import whatsupdocker from "./whatsupdocker/widget"; @@ -279,6 +280,7 @@ const widgets = { uptimerobot, urbackup, vikunja, + wallos, watchtower, wgeasy, whatsupdocker,