From 60c6123dc8628d0cb70f2e1ca429e46a3987701b Mon Sep 17 00:00:00 2001 From: vhsdream <67816022+vhsdream@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:35:19 -0400 Subject: [PATCH] Initial commit of Vikunja widget --- docs/widgets/services/vikunja.md | 19 +++++++++ public/locales/en/common.json | 4 ++ src/utils/proxy/handlers/credentialed.js | 1 + src/widgets/components.js | 1 + src/widgets/vikunja/component.jsx | 37 ++++++++++++++++++ src/widgets/vikunja/proxy.js | 49 ++++++++++++++++++++++++ src/widgets/vikunja/widget.js | 26 +++++++++++++ src/widgets/widgets.js | 2 + 8 files changed, 139 insertions(+) create mode 100644 docs/widgets/services/vikunja.md create mode 100644 src/widgets/vikunja/component.jsx create mode 100644 src/widgets/vikunja/proxy.js create mode 100644 src/widgets/vikunja/widget.js diff --git a/docs/widgets/services/vikunja.md b/docs/widgets/services/vikunja.md new file mode 100644 index 000000000..6bfc3c9ce --- /dev/null +++ b/docs/widgets/services/vikunja.md @@ -0,0 +1,19 @@ +--- +title: Vikunja +description: Vikunja Widget Configuration +--- + +Learn more about [Vikunja](https://vikunja.io). + +Allowed fields: `["projects", "tasks"]`. + +"Projects" lists the number of non-archived Projects the user has access to. + +"Tasks" lists the number of tasks due within the next 7 days. + +```yaml +widget: + type: vikunja + url: http[s]://vikunja.host.or.ip[:port] + key: vikunjaapikey +``` diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3aea07ebb..acbf4d28d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -953,5 +953,9 @@ "reminders": "Reminders", "nextReminder": "Next Reminder", "none": "None" + }, + "vikunja": { + "projects": "Total Active Projects", + "tasks": "Tasks Due This Week" } } diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 398220753..d59ffac27 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -44,6 +44,7 @@ export default async function credentialedProxyHandler(req, res, map) { "tailscale", "tandoor", "pterodactyl", + "vikunja", ].includes(widget.type) ) { headers.Authorization = `Bearer ${widget.key}`; diff --git a/src/widgets/components.js b/src/widgets/components.js index 0a5a815cb..62bd479f5 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -125,6 +125,7 @@ const components = { uptimekuma: dynamic(() => import("./uptimekuma/component")), uptimerobot: dynamic(() => import("./uptimerobot/component")), urbackup: dynamic(() => import("./urbackup/component")), + vikunja: dynamic(() => import("./vikunja/component")), watchtower: dynamic(() => import("./watchtower/component")), wgeasy: dynamic(() => import("./wgeasy/component")), whatsupdocker: dynamic(() => import("./whatsupdocker/component")), diff --git a/src/widgets/vikunja/component.jsx b/src/widgets/vikunja/component.jsx new file mode 100644 index 000000000..f8d537308 --- /dev/null +++ b/src/widgets/vikunja/component.jsx @@ -0,0 +1,37 @@ +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: projectsData, error: projectsError } = useWidgetAPI(widget, "projects"); + const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "tasks", { + filter: "done=false&&due_date<=now+7d", + }); + + if (projectsError || tasksError) { + const vikunjaError = projectsError ?? tasksError; + return ; + } + + if (!projectsData || !tasksData) { + return ( + + + + + ); + } + + return ( + + + + + ); +} diff --git a/src/widgets/vikunja/proxy.js b/src/widgets/vikunja/proxy.js new file mode 100644 index 000000000..41b46a9bc --- /dev/null +++ b/src/widgets/vikunja/proxy.js @@ -0,0 +1,49 @@ +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 = "vikunjaProxyHandler"; +const logger = createLogger(proxyName); + +export default async function vikunjaProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (!group || !service) { + logger.error("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 || !widgets[widget.type].api) { + logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid widget configuration" }); + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + + try { + const params = { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${widget.token}`, + }, + }; + + logger.debug("Calling Vikunja API endpoint: %s", endpoint); + + const [status, , data] = await httpProxy(url, params); + + if (status !== 200) { + logger.error("Error calling Vikunja API: %d. Data: %s", status, data); + return res.status(status).json({ error: "Vikunja API Error", data }); + } + + return res.status(status).send(data); + } catch (error) { + logger.error("Exception calling Vikunja API: %s", error.message); + return res.status(500).json({ error: "Vikunja API Error", message: error.message }); + } +} diff --git a/src/widgets/vikunja/widget.js b/src/widgets/vikunja/widget.js new file mode 100644 index 000000000..fa6bf2951 --- /dev/null +++ b/src/widgets/vikunja/widget.js @@ -0,0 +1,26 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; +import { jsonArrayFilter } from "utils/proxy/api-helpers"; + +const widget = { + api: `{url}/api/v1/{endpoint}`, + proxyHandler: credentialedProxyHandler, + + mappings: { + projects: { + endpoint: "projects", + map: (data) => ({ + projects: jsonArrayFilter(data, (item) => !item.isArchived).length, + }), + }, + tasks: { + endpoint: "tasks/all", + // to filter by done=false and dueDate <= now+7d or whatever + params: ["filter"], + map: (data) => ({ + tasks: jsonArrayFilter(data, (item) => !item.done).length, + }), + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 3334e47e5..faff57eb4 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -115,6 +115,7 @@ import unifi from "./unifi/widget"; import unmanic from "./unmanic/widget"; import uptimekuma from "./uptimekuma/widget"; import uptimerobot from "./uptimerobot/widget"; +import vikunja from "./vikunja/widget"; import watchtower from "./watchtower/widget"; import wgeasy from "./wgeasy/widget"; import whatsupdocker from "./whatsupdocker/widget"; @@ -246,6 +247,7 @@ const widgets = { uptimekuma, uptimerobot, urbackup, + vikunja, watchtower, wgeasy, whatsupdocker,