From 4dca4cc892c495e0d5a888d72dde8ce000e070a3 Mon Sep 17 00:00:00 2001 From: sgrtye <55668018+sgrtye@users.noreply.github.com> Date: Thu, 15 Feb 2024 07:18:37 +0000 Subject: [PATCH 1/8] Feature: Add list view for custom api (#2891) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/customapi.md | 49 +++++++++++- src/utils/config/service-helpers.js | 2 + src/widgets/customapi/component.jsx | 116 +++++++++++++++++++++++----- 3 files changed, 146 insertions(+), 21 deletions(-) diff --git a/docs/widgets/services/customapi.md b/docs/widgets/services/customapi.md index d392f0a9c..29b72633e 100644 --- a/docs/widgets/services/customapi.md +++ b/docs/widgets/services/customapi.md @@ -16,6 +16,7 @@ widget: password: password # auth - optional method: GET # optional, e.g. POST headers: # optional, must be object, see below + display: # optional, default to block, see below mappings: - field: key # needs to be YAML string or object label: Field 1 @@ -43,6 +44,15 @@ widget: locale: nl # optional style: short # optional - defaults to "long". Allowed values: `["long", "short", "narrow"]`. numeric: auto # optional - defaults to "always". Allowed values `["always", "auto"]`. + - field: key + label: Field 6 + format: text + additionalField: # optional + field: + hourly: + time: other key + color: theme # optional - defaults to "". Allowed values: `["theme", "adaptive", "black", "white"]`. + format: date # optional ``` Supported formats for the values are `text`, `number`, `float`, `percent`, `bytes`, `bitrate`, `date` and `relativeDate`. @@ -93,7 +103,7 @@ mappings: ## Data Transformation -You can manipulate data with the following tools `remap`, `scale` and `suffix`, for example: +You can manipulate data with the following tools `remap`, `scale`, `prefix` and `suffix`, for example: ```yaml - field: key4 @@ -110,7 +120,42 @@ You can manipulate data with the following tools `remap`, `scale` and `suffix`, label: Power format: float scale: 0.001 # can be number or string e.g. 1/16 - suffix: kW + suffix: "kW" +- field: key6 + label: Price + format: float + prefix: "$" +``` + +## List View + +You can change the default block view to a list view by setting the `display` option to `list`. + +The list view can optionally display an additional field next to the primary field. + +`additionalField`: Similar to `field`, but only used in list view. Displays additional information for the mapping object on the right. + +`field`: Defined the same way as other custom api widget fields. + +`color`: Allowed options: `"theme", "adaptive", "black", "white"`. The option `adaptive` will apply a color using the value of the `additionalField`, green for positive numbers, red for negative numbers. + +```yaml +- field: key + label: Field + format: text + remap: + - value: 0 + to: None + - value: 1 + to: Connected + - any: true # will map all other values + to: Unknown + additionalField: + field: + hourly: + time: key + color: theme + format: date ``` ## Custom Headers diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index fb6757b6f..e203e6d7f 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -378,6 +378,7 @@ export function cleanServiceGroups(groups) { // customapi mappings, + display, // diskstation volume, @@ -539,6 +540,7 @@ export function cleanServiceGroups(groups) { } if (type === "customapi") { if (mappings) cleanedService.widget.mappings = mappings; + if (display) cleanedService.widget.display = display; if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; } if (type === "calendar") { diff --git a/src/widgets/customapi/component.jsx b/src/widgets/customapi/component.jsx index 7e6ad4d73..726dcb602 100644 --- a/src/widgets/customapi/component.jsx +++ b/src/widgets/customapi/component.jsx @@ -90,6 +90,12 @@ function formatValue(t, mapping, rawValue) { // nothing } + // Apply fixed prefix. + const prefix = mapping?.prefix; + if (prefix) { + value = `${prefix} ${value}`; + } + // Apply fixed suffix. const suffix = mapping?.suffix; if (suffix) { @@ -99,12 +105,35 @@ function formatValue(t, mapping, rawValue) { return value; } +function getColor(mapping, customData) { + const value = getValue(mapping.additionalField.field, customData); + const { color } = mapping.additionalField; + + switch (color) { + case "adaptive": + try { + const number = parseFloat(value); + return number > 0 ? "text-emerald-300" : "text-rose-300"; + } catch (e) { + return ""; + } + case "black": + return `text-black`; + case "white": + return `text-white`; + case "theme": + return `text-theme-500`; + default: + return ""; + } +} + export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; - const { mappings = [], refreshInterval = 10000 } = widget; + const { mappings = [], refreshInterval = 10000, display = "block" } = widget; const { data: customData, error: customError } = useWidgetAPI(widget, null, { refreshInterval: Math.max(1000, refreshInterval), }); @@ -114,24 +143,73 @@ export default function Component({ service }) { } if (!customData) { - return ( - - {mappings.slice(0, 4).map((item) => ( - - ))} - - ); + switch (display) { + case "list": + return ( + + + {mappings.map((mapping) => ( + + {mapping.label} + + - + + + ))} + + + ); + + default: + return ( + + {mappings.slice(0, 4).map((item) => ( + + ))} + + ); + } } - return ( - - {mappings.slice(0, 4).map((mapping) => ( - - ))} - - ); + switch (display) { + case "list": + return ( + + + {mappings.map((mapping) => ( + + {mapping.label} + + {formatValue(t, mapping, getValue(mapping.field, customData))} + {mapping.additionalField && ( + + {formatValue(t, mapping.additionalField, getValue(mapping.additionalField.field, customData))} + + )} + + + ))} + + + ); + + default: + return ( + + {mappings.slice(0, 4).map((mapping) => ( + + ))} + + ); + } } From a251c3405978fccb1e748a4026f68053001e8a8f Mon Sep 17 00:00:00 2001 From: Benedek Kozma Date: Fri, 16 Feb 2024 00:52:59 +0100 Subject: [PATCH 2/8] Change: move custom.css linking to head to avoid FOUC (#2916) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- src/components/filecontent.jsx | 10 ---------- src/pages/index.jsx | 12 ++---------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 src/components/filecontent.jsx diff --git a/src/components/filecontent.jsx b/src/components/filecontent.jsx deleted file mode 100644 index 1dd6266a2..000000000 --- a/src/components/filecontent.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import useSWR from "swr"; - -export default function FileContent({ path, loadingValue, errorValue, emptyValue = "" }) { - const fetcher = (url) => fetch(url).then((res) => res.text()); - const { data, error, isLoading } = useSWR(`/api/config/${path}`, fetcher); - - if (error) return errorValue; - if (isLoading) return loadingValue; - return data || emptyValue; -} diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 4bf1d9a27..39ac6cf29 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -11,7 +11,6 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; import Tab, { slugify } from "components/tab"; -import FileContent from "components/filecontent"; import ServicesGroup from "components/services/group"; import BookmarksGroup from "components/bookmarks/group"; import Widget from "components/widgets/widget"; @@ -391,17 +390,10 @@ function Home({ initialSettings }) { )} + + {/* eslint-disable-line @next/next/no-css-tags */} - - From 0d47dcaac7697409c38b9e6e4d9d3931b278ac6c Mon Sep 17 00:00:00 2001 From: Derek Stotz Date: Thu, 15 Feb 2024 23:38:55 -0600 Subject: [PATCH 3/8] Enhancement: Add enablePools option to TrueNAS service widget (#2908) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/truenas.md | 3 ++ src/utils/config/service-helpers.js | 6 ++++ src/utils/proxy/handlers/credentialed.js | 10 +++++-- src/widgets/truenas/component.jsx | 24 ++++++++++----- src/widgets/truenas/pool.jsx | 31 +++++++++++++++++++ src/widgets/truenas/widget.js | 38 ++++++++---------------- 6 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 src/widgets/truenas/pool.jsx diff --git a/docs/widgets/services/truenas.md b/docs/widgets/services/truenas.md index 6d747ef17..243504905 100644 --- a/docs/widgets/services/truenas.md +++ b/docs/widgets/services/truenas.md @@ -9,6 +9,8 @@ Allowed fields: `["load", "uptime", "alerts"]`. To create an API Key, follow [the official TrueNAS documentation](https://www.truenas.com/docs/scale/scaletutorials/toptoolbar/managingapikeys/). +A detailed pool listing is disabled by default, but can be enabled with the `enablePools` option. + ```yaml widget: type: truenas @@ -16,4 +18,5 @@ widget: username: user # not required if using api key password: pass # not required if using api key key: yourtruenasapikey # not required if using username / password + enablePools: true # optional, defaults to false ``` diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index e203e6d7f..67502a7a7 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -442,6 +442,9 @@ export function cleanServiceGroups(groups) { // sonarr, radarr enableQueue, + // truenas + enablePools, + // unifi site, } = cleanedService.widget; @@ -511,6 +514,9 @@ export function cleanServiceGroups(groups) { 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 (["diskstation", "qnap"].includes(type)) { if (volume) cleanedService.widget.volume = volume; } diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 0795efd52..bc0875932 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -29,11 +29,15 @@ export default async function credentialedProxyHandler(req, res, map) { } else if (widget.type === "gotify") { headers["X-gotify-Key"] = `${widget.key}`; } else if ( - ["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "truenas", "pterodactyl"].includes( - widget.type, - ) + ["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "pterodactyl"].includes(widget.type) ) { headers.Authorization = `Bearer ${widget.key}`; + } else if (widget.type === "truenas") { + if (widget.key) { + headers.Authorization = `Bearer ${widget.key}`; + } else { + headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; + } } else if (widget.type === "proxmox") { headers.Authorization = `PVEAPIToken=${widget.username}=${widget.password}`; } else if (widget.type === "proxmoxbackupserver") { diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx index c1fc5c53a..872d8c647 100644 --- a/src/widgets/truenas/component.jsx +++ b/src/widgets/truenas/component.jsx @@ -3,6 +3,7 @@ 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"; +import Pool from "widgets/truenas/pool"; export default function Component({ service }) { const { t } = useTranslation(); @@ -11,9 +12,10 @@ export default function Component({ service }) { const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts"); const { data: statusData, error: statusError } = useWidgetAPI(widget, "status"); + const { data: poolsData, error: poolsError } = useWidgetAPI(widget, "pools"); - if (alertError || statusError) { - const finalError = alertError ?? statusError; + if (alertError || statusError || poolsError) { + const finalError = alertError ?? statusError ?? poolsError; return ; } @@ -27,11 +29,19 @@ export default function Component({ service }) { ); } + const enablePools = widget?.enablePools && Array.isArray(poolsData) && poolsData.length > 0; + return ( - - - - - + <> + + + + + + {enablePools && + poolsData.map((pool) => ( + + ))} + > ); } diff --git a/src/widgets/truenas/pool.jsx b/src/widgets/truenas/pool.jsx new file mode 100644 index 000000000..8e9d04656 --- /dev/null +++ b/src/widgets/truenas/pool.jsx @@ -0,0 +1,31 @@ +import classNames from "classnames"; +import prettyBytes from "pretty-bytes"; + +export default function Pool({ name, free, allocated, healthy }) { + const total = free + allocated; + const usedPercent = Math.round((allocated / total) * 100); + const statusColor = healthy ? "bg-green-500" : "bg-yellow-500"; + + return ( + + + + + + + {name} + + + + {prettyBytes(allocated)} / {prettyBytes(total)} + + ({usedPercent}%) + + + ); +} diff --git a/src/widgets/truenas/widget.js b/src/widgets/truenas/widget.js index 6c0f3622c..7435b6e19 100644 --- a/src/widgets/truenas/widget.js +++ b/src/widgets/truenas/widget.js @@ -1,32 +1,9 @@ -import { jsonArrayFilter } from "utils/proxy/api-helpers"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; -import genericProxyHandler from "utils/proxy/handlers/generic"; -import getServiceWidget from "utils/config/service-helpers"; +import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v2.0/{endpoint}", - proxyHandler: async (req, res, map) => { - // choose proxy handler based on widget settings - const { group, service } = req.query; - - if (group && service) { - const widgetOpts = await getServiceWidget(group, service); - let handler; - if (widgetOpts.username && widgetOpts.password) { - handler = genericProxyHandler; - } else if (widgetOpts.key) { - handler = credentialedProxyHandler; - } - - if (handler) { - return handler(req, res, map); - } - - return res.status(500).json({ error: "Username / password or API key required" }); - } - - return res.status(500).json({ error: "Error parsing widget request" }); - }, + proxyHandler: credentialedProxyHandler, mappings: { alerts: { @@ -39,6 +16,17 @@ const widget = { endpoint: "system/info", validate: ["loadavg", "uptime_seconds"], }, + pools: { + endpoint: "pool", + map: (data) => + asJson(data).map((entry) => ({ + id: entry.name, + name: entry.name, + healthy: entry.healthy, + allocated: entry.allocated, + free: entry.free, + })), + }, }, }; From c268739e1f6d160f60113f46daf7f6bc2897b512 Mon Sep 17 00:00:00 2001 From: Derek Stotz Date: Fri, 16 Feb 2024 22:25:31 -0600 Subject: [PATCH 4/8] Enhancement: Add requestBody param for customapi (#2921) --- docs/widgets/services/customapi.md | 14 ++++++++++++++ src/utils/proxy/handlers/generic.js | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/docs/widgets/services/customapi.md b/docs/widgets/services/customapi.md index 29b72633e..7f26f80fb 100644 --- a/docs/widgets/services/customapi.md +++ b/docs/widgets/services/customapi.md @@ -16,6 +16,7 @@ widget: password: password # auth - optional method: GET # optional, e.g. POST headers: # optional, must be object, see below + requestBody: # optional, can be string or object, see below display: # optional, default to block, see below mappings: - field: key # needs to be YAML string or object @@ -166,3 +167,16 @@ Pass custom headers using the `headers` option, for example: headers: X-API-Token: token ``` + +## Custom Request Body + +Pass custom request body using the `requestBody` option in either a string or object format. Objects will automatically be converted to a JSON string. + +```yaml +requestBody: + foo: bar +# or +requestBody: "{\"foo\":\"bar\"}" +``` + +Both formats result in `{"foo":"bar"}` being sent as the request body. Don't forget to set your `Content-Type` headers! diff --git a/src/utils/proxy/handlers/generic.js b/src/utils/proxy/handlers/generic.js index 8b91049fd..e4717469a 100644 --- a/src/utils/proxy/handlers/generic.js +++ b/src/utils/proxy/handlers/generic.js @@ -35,6 +35,12 @@ export default async function genericProxyHandler(req, res, map) { }; if (req.body) { params.body = req.body; + } else if (widget.requestBody) { + if (typeof widget.requestBody === "object") { + params.body = JSON.stringify(widget.requestBody); + } else { + params.body = widget.requestBody; + } } const [status, contentType, data] = await httpProxy(url, params); From 36585a2c44acda10bea1c7a1efe0ef78956b213d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 17 Feb 2024 21:49:51 -0800 Subject: [PATCH 5/8] Close unsupported feature requests --- .github/workflows/repo-maintenance.yml | 78 ++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml index d590cf912..deb81ad4d 100644 --- a/.github/workflows/repo-maintenance.yml +++ b/.github/workflows/repo-maintenance.yml @@ -194,6 +194,84 @@ jobs: } await github.graphql(closeDiscussionMutation, closeVariables); + await sleep(1000); + } + } + close-unsupported-feature-requests: + name: 'Close Unsupported Feature Requests' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + const CUTOFF_1_DAYS = 180; + const CUTOFF_1_COUNT = 5; + const CUTOFF_2_DAYS = 365; + const CUTOFF_2_COUNT = 10; + + const cutoff1Date = new Date(); + cutoff1Date.setDate(cutoff1Date.getDate() - CUTOFF_1_DAYS); + const cutoff2Date = new Date(); + cutoff2Date.setDate(cutoff2Date.getDate() - CUTOFF_2_DAYS); + + const query = `query( + $owner:String!, + $name:String!, + $featureRequestsCategory:ID!, + ) { + repository(owner:$owner, name:$name){ + discussions( + categoryId:$featureRequestsCategory, + last:100, + states:[OPEN], + ) { + nodes { + id, + number, + updatedAt, + upvoteCount, + } + }, + } + }`; + const variables = { + owner: context.repo.owner, + name: context.repo.repo, + featureRequestsCategory: "DIC_kwDOH31rQM4CRErS" + } + const result = await github.graphql(query, variables); + + for (const discussion of result.repository.discussions.nodes) { + const discussionDate = new Date(discussion.updatedAt); + if ((discussionDate < cutoff1Date && discussion.upvoteCount < CUTOFF_1_COUNT) || + (discussionDate < cutoff2Date && discussion.upvoteCount < CUTOFF_2_COUNT)) { + console.log(`Closing discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt} with votes ${discussion.upvoteCount}`); + const addCommentMutation = `mutation($discussion:ID!, $body:String!) { + addDiscussionComment(input:{discussionId:$discussion, body:$body}) { + clientMutationId + } + }`; + const commentVariables = { + discussion: discussion.id, + body: 'This discussion has been automatically closed due to lack of community interest.', + } + await github.graphql(addCommentMutation, commentVariables); + + const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) { + closeDiscussion(input:{discussionId:$discussion, reason:$reason}) { + clientMutationId + } + }`; + const closeVariables = { + discussion: discussion.id, + reason: "OUTDATED", + } + await github.graphql(closeDiscussionMutation, closeVariables); + await sleep(1000); } } From b0dd810b490d2888346024d252e0b9de2b324293 Mon Sep 17 00:00:00 2001 From: Steven de Jong Date: Sun, 18 Feb 2024 23:14:23 +0100 Subject: [PATCH 6/8] Enhancement: allow api auth with moonraker service (#2934) --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/moonraker.md | 9 +++++++++ src/widgets/moonraker/widget.js | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/widgets/services/moonraker.md b/docs/widgets/services/moonraker.md index 6de62ec64..2ee1a4e2a 100644 --- a/docs/widgets/services/moonraker.md +++ b/docs/widgets/services/moonraker.md @@ -12,3 +12,12 @@ widget: type: moonraker url: http://moonraker.host.or.ip:port ``` + +If your moonraker instance has an active authorization and your homepage ip isn't whitelisted you need to add your api key ([Authorization Documentation](https://moonraker.readthedocs.io/en/latest/web_api/#authorization)). + +```yaml +widget: + type: moonraker + url: http://moonraker.host.or.ip:port + key: api_keymoonraker +``` diff --git a/src/widgets/moonraker/widget.js b/src/widgets/moonraker/widget.js index 314a8670e..d7673bed8 100644 --- a/src/widgets/moonraker/widget.js +++ b/src/widgets/moonraker/widget.js @@ -1,8 +1,8 @@ -import genericProxyHandler from "utils/proxy/handlers/generic"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; const widget = { api: "{url}/printer/objects/query?{endpoint}", - proxyHandler: genericProxyHandler, + proxyHandler: credentialedProxyHandler, mappings: { print_stats: { From 841c74d58a5735d66d136870fe9133e5d4253f75 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 19 Feb 2024 06:09:07 -0800 Subject: [PATCH 7/8] Add info re automatic closing --- .github/workflows/repo-maintenance.yml | 11 +++++++---- CONTRIBUTING.md | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml index deb81ad4d..d1f7e4fdd 100644 --- a/.github/workflows/repo-maintenance.yml +++ b/.github/workflows/repo-maintenance.yml @@ -27,7 +27,7 @@ jobs: stale-issue-message: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. + for your contributions. See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details. lock-threads: name: 'Lock Old Threads' runs-on: ubuntu-latest @@ -42,14 +42,17 @@ jobs: This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion for related concerns. + See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details. pr-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion for related concerns. + See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details. discussion-comment: > This discussion has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion for related concerns. + See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details. close-answered-discussions: name: 'Close Answered Discussions' runs-on: ubuntu-latest @@ -89,7 +92,7 @@ jobs: }`; const commentVariables = { discussion: discussion.id, - body: 'This discussion has been automatically closed because it was marked as answered.', + body: 'This discussion has been automatically closed because it was marked as answered. See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.', } await github.graphql(addCommentMutation, commentVariables) @@ -179,7 +182,7 @@ jobs: }`; const commentVariables = { discussion: discussion.id, - body: 'This discussion has been automatically closed due to inactivity.', + body: 'This discussion has been automatically closed due to inactivity. See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.', } await github.graphql(addCommentMutation, commentVariables); @@ -257,7 +260,7 @@ jobs: }`; const commentVariables = { discussion: discussion.id, - body: 'This discussion has been automatically closed due to lack of community interest.', + body: 'This discussion has been automatically closed due to lack of community support. See our [contributing guidelines](https://github.com/gethomepage/homepage/blob/main/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.', } await github.graphql(addCommentMutation, commentVariables); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51ca1f83e..f2361c434 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,3 +51,18 @@ By contributing, you agree that your contributions will be licensed under its GN ## References This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/main/CONTRIBUTING.md) + +# Automatic Respoistory Maintenance + +The homepage team appreciates all effort and interest from the community in filing bug reports, creating feature requests, sharing ideas and helping other community members. That said, in an effort to keep the repository organized and managebale the project uses automatic handling of certain areas: + +- Issues that cannot be reproduced will be marked 'stale' after 7 days of inactivity and closed after 14 further days of inactivity. +- Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity. +- Discussions with a marked answer will be automatically closed. +- Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity. +- Feature requests that do not meet the following thresholds will be closed: 5 "up-votes" after 180 days of inactivity or 10 "up-votes" after 365 days. + +In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns. +Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features. + +Thank you all for your contributions. From 619f365c9233dde2a353b508f16f3ae1d8d16abc Mon Sep 17 00:00:00 2001 From: Massimiliano De Luise <66636702+MDeLuise@users.noreply.github.com> Date: Tue, 20 Feb 2024 02:54:28 +0100 Subject: [PATCH 8/8] Feature: Plant-it widget (#2941) --- docs/widgets/services/planit.md | 15 ++++++++++ mkdocs.yml | 1 + public/locales/en/common.json | 6 ++++ public/locales/it/common.json | 6 ++++ src/utils/proxy/handlers/credentialed.js | 2 ++ src/widgets/components.js | 1 + src/widgets/plantit/component.jsx | 37 ++++++++++++++++++++++++ src/widgets/plantit/widget.js | 21 ++++++++++++++ src/widgets/widgets.js | 2 ++ 9 files changed, 91 insertions(+) create mode 100644 docs/widgets/services/planit.md create mode 100644 src/widgets/plantit/component.jsx create mode 100644 src/widgets/plantit/widget.js diff --git a/docs/widgets/services/planit.md b/docs/widgets/services/planit.md new file mode 100644 index 000000000..d1cebfaad --- /dev/null +++ b/docs/widgets/services/planit.md @@ -0,0 +1,15 @@ +--- +title: Plant-it +description: Plant-it Widget Configuration +--- + +Learn more about [Plantit](https://github.com/MDeLuise/plant-it). + +API key can be created from the REST API. + +```yaml +widget: + type: plantit + url: http://plant-it.host.or.ip:port # api port + key: plantit-api-key +``` diff --git a/mkdocs.yml b/mkdocs.yml index 572d36b4c..e9d531ba0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -103,6 +103,7 @@ nav: - widgets/services/photoprism.md - widgets/services/pialert.md - widgets/services/pihole.md + - widgets/services/plantit.md - widgets/services/plex-tautulli.md - widgets/services/plex.md - widgets/services/portainer.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5521fd0c3..cc6846a00 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -825,5 +825,11 @@ "netdata": { "warnings": "Warnings", "criticals": "Criticals" + }, + "plantit": { + "events": "Events", + "plants": "Plants", + "photos": "Photos", + "species": "Species" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 6f10baf7b..e61fff8b6 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -803,5 +803,11 @@ "netdata": { "warnings": "Warnings", "criticals": "Criticals" + }, + "plantit": { + "events": "Eventi", + "plants": "Piante", + "species": "Specie", + "images": "Immagini" } } diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index bc0875932..02338b828 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -65,6 +65,8 @@ export default async function credentialedProxyHandler(req, res, map) { headers.Authorization = `Basic ${Buffer.from(`$:${widget.key}`).toString("base64")}`; } else if (widget.type === "glances") { headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; + } else if (widget.type === "plantit") { + headers.Key = `${widget.key}`; } else { headers["X-API-Key"] = `${widget.key}`; } diff --git a/src/widgets/components.js b/src/widgets/components.js index bb9b00fe3..784f05b20 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -78,6 +78,7 @@ const components = { proxmoxbackupserver: dynamic(() => import("./proxmoxbackupserver/component")), pialert: dynamic(() => import("./pialert/component")), pihole: dynamic(() => import("./pihole/component")), + plantit: dynamic(() => import("./plantit/component")), plex: dynamic(() => import("./plex/component")), portainer: dynamic(() => import("./portainer/component")), prometheus: dynamic(() => import("./prometheus/component")), diff --git a/src/widgets/plantit/component.jsx b/src/widgets/plantit/component.jsx new file mode 100644 index 000000000..ea203b87b --- /dev/null +++ b/src/widgets/plantit/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: plantitData, error: plantitError } = useWidgetAPI(widget, "plantit"); + + if (plantitError) { + return ; + } + + if (!plantitData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} diff --git a/src/widgets/plantit/widget.js b/src/widgets/plantit/widget.js new file mode 100644 index 000000000..5a4bebc15 --- /dev/null +++ b/src/widgets/plantit/widget.js @@ -0,0 +1,21 @@ +import { asJson } from "utils/proxy/api-helpers"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + plantit: { + endpoint: "stats", + }, + map: (data) => ({ + events: Object.values(asJson(data).diaryEntryCount).reduce((acc, i) => acc + i, 0), + plants: Object.values(asJson(data).plantCount).reduce((acc, i) => acc + i, 0), + photos: Object.values(asJson(data).imageCount).reduce((acc, i) => acc + i, 0), + species: Object.values(asJson(data).botanicalInfoCount).reduce((acc, i) => acc + i, 0), + }), + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index fe474406c..6f50d9ef5 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -71,6 +71,7 @@ import photoprism from "./photoprism/widget"; import proxmoxbackupserver from "./proxmoxbackupserver/widget"; import pialert from "./pialert/widget"; import pihole from "./pihole/widget"; +import plantit from "./plantit/widget"; import plex from "./plex/widget"; import portainer from "./portainer/widget"; import prometheus from "./prometheus/widget"; @@ -180,6 +181,7 @@ const widgets = { proxmoxbackupserver, pialert, pihole, + plantit, plex, portainer, prometheus,