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,