Merge branch 'gethomepage:main' into main
This commit is contained in:
commit
cea07a227c
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,12 +1,12 @@
|
|||||||
## Proposed change
|
## Proposed change
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please include a summary of the change. Screenshots and / or videos can also be helpful if appropriate.
|
Please include a summary of the change. Screenshots and/or videos can also be helpful if appropriate.
|
||||||
|
|
||||||
*** Please see the development guidelines for new widgets: https://gethomepage.dev/latest/more/development/#service-widget-guidelines
|
*** Please see the development guidelines for new widgets: https://gethomepage.dev/latest/more/development/#service-widget-guidelines
|
||||||
*** If you do not follow these guidelines your PR will likely be closed without review.
|
*** If you do not follow these guidelines your PR will likely be closed without review.
|
||||||
|
|
||||||
New service widgets should include example(s) of relevant relevant API output as well updates to the docs for the new widget.
|
New service widgets should include example(s) of relevant API output as well as updates to the docs for the new widget.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Closes # (issue)
|
Closes # (issue)
|
||||||
|
|||||||
@ -41,7 +41,7 @@ With features like quick search, bookmarks, weather support, a wide range of int
|
|||||||
|
|
||||||
## Docker Integration
|
## Docker Integration
|
||||||
|
|
||||||
Homepage has built-in support for Docker, and can automatically discover and add services to the homepage based on labels. See the [Docker](https://gethomepage.dev/latest/installation/docker/) page for more information.
|
Homepage has built-in support for Docker, and can automatically discover and add services to the homepage based on labels. See the [Docker Service Discovery](https://gethomepage.dev/latest/configs/docker/#automatic-service-discovery) page for more information.
|
||||||
|
|
||||||
## Service Widgets
|
## Service Widgets
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,10 @@ Learn more about [Azure DevOps](https://azure.microsoft.com/en-us/products/devop
|
|||||||
|
|
||||||
This widget has 2 functions:
|
This widget has 2 functions:
|
||||||
|
|
||||||
1. Pipelines: checks if the relevant pipeline is running or not, and if not, reports the last status.\
|
1. Pipelines: checks if the relevant pipeline is running or not, and if not, reports the last status.<br>
|
||||||
Allowed fields: `["result", "status"]`.
|
Allowed fields: `["result", "status"]`.
|
||||||
|
|
||||||
2. Pull Requests: returns the amount of open PRs, the amount of the PRs you have open, and how many PRs that you open are marked as 'Approved' by at least 1 person and not yet completed.\
|
2. Pull Requests: returns the amount of open PRs, the amount of the PRs you have open, and how many PRs that you open are marked as 'Approved' by at least 1 person and not yet completed.<br>
|
||||||
Allowed fields: `["totalPrs", "myPrs", "approved"]`.
|
Allowed fields: `["totalPrs", "myPrs", "approved"]`.
|
||||||
|
|
||||||
You will need to generate a personal access token for an existing user, see the [azure documentation](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#create-a-pat)
|
You will need to generate a personal access token for an existing user, see the [azure documentation](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#create-a-pat)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Learn more about [Crowdsec](https://crowdsec.net).
|
|||||||
See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines) for information about registering a machine,
|
See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines) for information about registering a machine,
|
||||||
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
|
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
|
||||||
|
|
||||||
Allowed fields: ["alerts", "bans"]
|
Allowed fields: `["alerts", "bans"]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@ -11,22 +11,27 @@ An optional 'volume' parameter can be supplied to specify which volume's free sp
|
|||||||
|
|
||||||
Allowed fields: `["uptime", "volumeAvailable", "resources.cpu", "resources.mem"]`.
|
Allowed fields: `["uptime", "volumeAvailable", "resources.cpu", "resources.mem"]`.
|
||||||
|
|
||||||
To access these system metrics you need to connect to the DiskStation with an account that is a member of the default `Administrators` group. That is because these metrics are requested from the API's `SYNO.Core.System` part that is only available to admin users. In order to keep the security impact as small as possible we can set the account in DSM up to limit the user's permissions inside the Synology system. In DSM 7.x, for instance, follow these steps:
|
To access these system metrics you need to connect to the DiskStation (`DSM`) with an account that is a member of the default `Administrators` group. That is because these metrics are requested from the API's `SYNO.Core.System` part that is only available to admin users. In order to keep the security impact as small as possible we can set the account in DSM up to limit the user's permissions inside the Synology system. In DSM 7.x, for instance, follow these steps:
|
||||||
|
|
||||||
1. Create a new user, i.e. `remote_stats`.
|
1. Create a new user, i.e. `remote_stats`.
|
||||||
2. Set up a strong password for the new user
|
2. Set up a strong password for the new user
|
||||||
3. Under the `User Groups` tab of the user config dialogue check the box for `Administrators`.
|
3. Under the `User Groups` tab of the user config dialogue check the box for `Administrators`.
|
||||||
4. On the `Permissions` tab check the top box for `No Access`, effectively prohibiting the user from accessing anything in the shared folders.
|
4. On the `Permissions` tab check the top box for `No Access`, effectively prohibiting the user from accessing anything in the shared folders.
|
||||||
5. Under `Applications` check the box next to `Deny` in the header to explicitly prohibit login to all applications.
|
5. Under `Applications` check the box next to `Deny` in the header to explicitly prohibit login to all applications.
|
||||||
6. Now _only_ allow login to the `Download Station` application, either by
|
6. Now _only_ allow login to the `DSM` application, either by
|
||||||
- unchecking `Deny` in the respective row, or (if inheriting permission doesn't work because of other group settings)
|
- unchecking `Deny` in the respective row, or (if inheriting permission doesn't work because of other group settings)
|
||||||
- checking `Allow` for this app, or
|
- checking `Allow` for this app, or
|
||||||
- checking `By IP` for this app to limit the source of login attempts to one or more IP addresses/subnets.
|
- checking `By IP` for this app to limit the source of login attempts to one or more IP addresses/subnets.
|
||||||
7. When the `Preview` column shows `Allow` in the `Download Station` row, click `Save`.
|
7. When the `Preview` column shows `Allow` in the `DSM` row, click `Save`.
|
||||||
|
|
||||||
Now configure the widget with the correct login information and test it.
|
Now configure the widget with the correct login information and test it.
|
||||||
|
|
||||||
If you encounter issues during testing, make sure to uncheck the option for automatic blocking due to invalid logins under `Control Panel > Security > Protection`. If desired, this setting can be reactivated once the login is established working.
|
If you encounter issues during testing:
|
||||||
|
|
||||||
|
1. Make sure to uncheck the option for automatic blocking due to invalid logins under `Control Panel > Security > Protection`.
|
||||||
|
- If desired, this setting can be reactivated once the login is established working.
|
||||||
|
2. Login to your Synology DSM with the newly created account and accept terms and conditions.
|
||||||
|
3. Reattempt
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Learn more about [Gitea](https://gitea.com).
|
|||||||
|
|
||||||
API token requires `notifications`, `repository` and `issue` permissions. See the [gitea documentation](https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens) for details on generating tokens.
|
API token requires `notifications`, `repository` and `issue` permissions. See the [gitea documentation](https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens) for details on generating tokens.
|
||||||
|
|
||||||
Allowed fields: ["notifications", "issues", "pulls"]
|
Allowed fields: `["notifications", "issues", "pulls"]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@ -9,7 +9,7 @@ This widget adds support for [Network UPS Tools](https://networkupstools.org/) v
|
|||||||
|
|
||||||
The default ups name is `ups`. To configure more than one ups, you must create multiple peanut services.
|
The default ups name is `ups`. To configure more than one ups, you must create multiple peanut services.
|
||||||
|
|
||||||
Allowed fields: `["battery_charge", "ups_load", "ups_status"]`
|
Allowed fields: `["battery_charge", "ups_load", "ups_status"]`.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ Note: by default the "blocked" and "blocked_percent" fields are merged e.g. "1,2
|
|||||||
widget:
|
widget:
|
||||||
type: pihole
|
type: pihole
|
||||||
url: http://pi.hole.or.ip
|
url: http://pi.hole.or.ip
|
||||||
|
version: 6 # required if running v6 or higher, defaults to 5
|
||||||
key: yourpiholeapikey # optional
|
key: yourpiholeapikey # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ description: Prometheus Widget Configuration
|
|||||||
|
|
||||||
Learn more about [Prometheus](https://github.com/prometheus/prometheus).
|
Learn more about [Prometheus](https://github.com/prometheus/prometheus).
|
||||||
|
|
||||||
Allowed fields: `["targets_up", "targets_down", "targets_total"]`
|
Allowed fields: `["targets_up", "targets_down", "targets_total"]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@ -5,7 +5,7 @@ description: Pterodactyl Widget Configuration
|
|||||||
|
|
||||||
Learn more about [Pterodactyl](https://github.com/pterodactyl).
|
Learn more about [Pterodactyl](https://github.com/pterodactyl).
|
||||||
|
|
||||||
Allowed fields: `["nodes", "servers"]`
|
Allowed fields: `["nodes", "servers"]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@ -11,6 +11,8 @@ To create an API Key, follow [the official TrueNAS documentation](https://www.tr
|
|||||||
|
|
||||||
A detailed pool listing is disabled by default, but can be enabled with the `enablePools` option.
|
A detailed pool listing is disabled by default, but can be enabled with the `enablePools` option.
|
||||||
|
|
||||||
|
To use the `enablePools` option with TrueNAS Core, the `nasType` parameter is required.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
type: truenas
|
type: truenas
|
||||||
@ -19,4 +21,5 @@ widget:
|
|||||||
password: pass # not required if using api key
|
password: pass # not required if using api key
|
||||||
key: yourtruenasapikey # not required if using username / password
|
key: yourtruenasapikey # not required if using username / password
|
||||||
enablePools: true # optional, defaults to false
|
enablePools: true # optional, defaults to false
|
||||||
|
nasType: scale # defaults to scale, must be set to 'core' if using enablePools with TrueNAS Core
|
||||||
```
|
```
|
||||||
|
|||||||
660
package-lock.json
generated
660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -16,7 +16,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"follow-redirects": "^1.15.5",
|
"follow-redirects": "^1.15.6",
|
||||||
"gamedig": "^4.3.1",
|
"gamedig": "^4.3.1",
|
||||||
"i18next": "^21.10.0",
|
"i18next": "^21.10.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^11.18.6",
|
"react-i18next": "^11.18.6",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"recharts": "^2.12.2",
|
"recharts": "^2.12.3",
|
||||||
"rrule": "^2.8.1",
|
"rrule": "^2.8.1",
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"systeminformation": "^5.22.0",
|
"systeminformation": "^5.22.0",
|
||||||
@ -52,12 +52,12 @@
|
|||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
553
pnpm-lock.yaml
553
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -2,11 +2,7 @@ import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
|
|||||||
|
|
||||||
export default function ContainerForm({ children = [], options, additionalClassNames = "", callback }) {
|
export default function ContainerForm({ children = [], options, additionalClassNames = "", callback }) {
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={callback} className={`${getAllClasses(options, additionalClassNames)} information-widget-form`}>
|
||||||
type="button"
|
|
||||||
onSubmit={callback}
|
|
||||||
className={`${getAllClasses(options, additionalClassNames)} information-widget-form`}
|
|
||||||
>
|
|
||||||
{getInnerBlock(children)}
|
{getInnerBlock(children)}
|
||||||
{getBottomBlock(children)}
|
{getBottomBlock(children)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -393,8 +393,10 @@ export function cleanServiceGroups(groups) {
|
|||||||
enableBlocks,
|
enableBlocks,
|
||||||
enableNowPlaying,
|
enableNowPlaying,
|
||||||
|
|
||||||
// glances
|
// glances, pihole
|
||||||
version,
|
version,
|
||||||
|
|
||||||
|
// glances
|
||||||
chart,
|
chart,
|
||||||
metric,
|
metric,
|
||||||
pointsLimit,
|
pointsLimit,
|
||||||
@ -448,6 +450,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
|
|
||||||
// truenas
|
// truenas
|
||||||
enablePools,
|
enablePools,
|
||||||
|
nasType,
|
||||||
|
|
||||||
// unifi
|
// unifi
|
||||||
site,
|
site,
|
||||||
@ -520,6 +523,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
}
|
}
|
||||||
if (type === "truenas") {
|
if (type === "truenas") {
|
||||||
if (enablePools !== undefined) cleanedService.widget.enablePools = JSON.parse(enablePools);
|
if (enablePools !== undefined) cleanedService.widget.enablePools = JSON.parse(enablePools);
|
||||||
|
if (nasType !== undefined) cleanedService.widget.nasType = nasType;
|
||||||
}
|
}
|
||||||
if (["diskstation", "qnap"].includes(type)) {
|
if (["diskstation", "qnap"].includes(type)) {
|
||||||
if (volume) cleanedService.widget.volume = volume;
|
if (volume) cleanedService.widget.volume = volume;
|
||||||
@ -528,8 +532,10 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost;
|
if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost;
|
||||||
if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath;
|
if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath;
|
||||||
}
|
}
|
||||||
if (type === "glances") {
|
if (["glances", "pihole"].includes(type)) {
|
||||||
if (version) cleanedService.widget.version = version;
|
if (version) cleanedService.widget.version = version;
|
||||||
|
}
|
||||||
|
if (type === "glances") {
|
||||||
if (metric) cleanedService.widget.metric = metric;
|
if (metric) cleanedService.widget.metric = metric;
|
||||||
if (chart !== undefined) {
|
if (chart !== undefined) {
|
||||||
cleanedService.widget.chart = chart;
|
cleanedService.widget.chart = chart;
|
||||||
|
|||||||
@ -20,12 +20,11 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(service.widget, "cpu", {
|
const { data, error } = useWidgetAPI(service.widget, `${version}/cpu`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, "quicklook", { version });
|
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|||||||
@ -24,9 +24,8 @@ export default function Component({ service }) {
|
|||||||
);
|
);
|
||||||
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(service.widget, "diskio", {
|
const { data, error } = useWidgetAPI(service.widget, `${version}/diskio`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const calculateRates = (d) =>
|
const calculateRates = (d) =>
|
||||||
|
|||||||
@ -15,9 +15,8 @@ export default function Component({ service }) {
|
|||||||
const [, fsName] = widget.metric.split("fs:");
|
const [, fsName] = widget.metric.split("fs:");
|
||||||
const diskUnits = widget.diskUnits === "bbytes" ? "common.bbytes" : "common.bytes";
|
const diskUnits = widget.diskUnits === "bbytes" ? "common.bbytes" : "common.bytes";
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(widget, "fs", {
|
const { data, error } = useWidgetAPI(widget, `${version}/fs`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@ -21,9 +21,8 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(widget, "gpu", {
|
const { data, error } = useWidgetAPI(widget, `${version}/gpu`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -76,14 +76,12 @@ export default function Component({ service }) {
|
|||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
|
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
|
||||||
|
|
||||||
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, "quicklook", {
|
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, "system", {
|
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, `${version}/system`, {
|
||||||
refreshInterval: defaultSystemInterval,
|
refreshInterval: defaultSystemInterval,
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (quicklookError) {
|
if (quicklookError) {
|
||||||
|
|||||||
@ -21,9 +21,8 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(service.widget, "mem", {
|
const { data, error } = useWidgetAPI(service.widget, `${version}/mem`, {
|
||||||
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -26,9 +26,8 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(widget, "network", {
|
const { data, error } = useWidgetAPI(widget, `${version}/network`, {
|
||||||
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -51,7 +50,7 @@ export default function Component({ service }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, interfaceName, pointsLimit]);
|
}, [data, interfaceName, pointsLimit, rxKey, txKey]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -26,9 +26,8 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const memoryInfoKey = version === 3 ? 0 : "data";
|
const memoryInfoKey = version === 3 ? 0 : "data";
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(service.widget, "processlist", {
|
const { data, error } = useWidgetAPI(service.widget, `${version}/processlist`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@ -21,9 +21,8 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||||
|
|
||||||
const { data, error } = useWidgetAPI(service.widget, "sensors", {
|
const { data, error } = useWidgetAPI(service.widget, `${version}/sensors`, {
|
||||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/api/{version}/{endpoint}",
|
api: "{url}/api/{endpoint}",
|
||||||
proxyHandler: credentialedProxyHandler,
|
proxyHandler: credentialedProxyHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ async function login(widget, service) {
|
|||||||
const endpoint = "auth/login";
|
const endpoint = "auth/login";
|
||||||
const api = widgets?.[widget.type]?.api;
|
const api = widgets?.[widget.type]?.api;
|
||||||
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||||
const loginBody = { username: widget.username, password: widget.password };
|
const loginBody = { username: widget.username.toString(), password: widget.password.toString() };
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
|
|
||||||
const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "summaryRaw");
|
const { data: piholeData, error: piholeError } = useWidgetAPI(widget);
|
||||||
|
|
||||||
if (piholeError) {
|
if (piholeError) {
|
||||||
return <Container service={service} error={piholeError} />;
|
return <Container service={service} error={piholeError} />;
|
||||||
|
|||||||
95
src/widgets/pihole/proxy.js
Normal file
95
src/widgets/pihole/proxy.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
|
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 = "piholeProxyHandler";
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
const sessionSIDCacheKey = `${proxyName}__sessionSID`;
|
||||||
|
|
||||||
|
async function login(widget, service) {
|
||||||
|
const url = formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "auth" });
|
||||||
|
const [status, , data] = await httpProxy(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: widget.key,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataParsed = JSON.parse(data);
|
||||||
|
|
||||||
|
if (status !== 200 || !dataParsed.session) {
|
||||||
|
logger.error("Failed to login to Pi-Hole API, status: %d", status);
|
||||||
|
cache.del(`${sessionSIDCacheKey}.${service}`);
|
||||||
|
} else {
|
||||||
|
cache.put(`${sessionSIDCacheKey}.${service}`, dataParsed.session.sid, dataParsed.session.validity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function piholeProxyHandler(req, res) {
|
||||||
|
const { group, service } = req.query;
|
||||||
|
let endpoint = "stats/summary";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: "Invalid widget configuration" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let status;
|
||||||
|
let data;
|
||||||
|
if (!widget.version || widget.version < 6) {
|
||||||
|
// pihole v5
|
||||||
|
endpoint = "summaryRaw";
|
||||||
|
[status, , data] = await httpProxy(formatApiCall(widgets[widget.type].apiv5, { ...widget, endpoint }));
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pihole v6
|
||||||
|
if (!cache.get(`${sessionSIDCacheKey}.${service}`)) {
|
||||||
|
await login(widget, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sid = cache.get(`${sessionSIDCacheKey}.${service}`);
|
||||||
|
if (!sid) {
|
||||||
|
return res.status(500).json({ error: "Failed to authenticate with Pi-hole" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("Calling Pi-hole API endpoint: %s", endpoint);
|
||||||
|
|
||||||
|
[status, , data] = await httpProxy(formatApiCall(widgets[widget.type].api, { ...widget, endpoint }), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-FTL-SID": sid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("Error calling Pi-Hole API: %d. Data: %s", status, data);
|
||||||
|
return res.status(status).json({ error: "Pi-Hole API Error", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataParsed = JSON.parse(data);
|
||||||
|
return res.status(status).json({
|
||||||
|
domains_being_blocked: dataParsed.gravity.domains_being_blocked,
|
||||||
|
ads_blocked_today: dataParsed.queries.blocked,
|
||||||
|
ads_percentage_today: dataParsed.queries.percent_blocked,
|
||||||
|
dns_queries_today: dataParsed.queries.total,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Exception calling Pi-Hole API: %s", error.message);
|
||||||
|
return res.status(500).json({ error: "Pi-Hole API Error", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,9 @@
|
|||||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
import piholeProxyHandler from "./proxy";
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
api: "{url}/admin/api.php?{endpoint}&auth={key}",
|
api: "{url}/api/{endpoint}",
|
||||||
proxyHandler: genericProxyHandler,
|
apiv5: "{url}/admin/api.php?{endpoint}&auth={key}",
|
||||||
|
proxyHandler: piholeProxyHandler,
|
||||||
mappings: {
|
|
||||||
summaryRaw: {
|
|
||||||
endpoint: "summaryRaw",
|
|
||||||
validate: ["dns_queries_today", "ads_blocked_today", "ads_percentage_today", "domains_being_blocked"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
export default widget;
|
||||||
|
|||||||
@ -40,7 +40,15 @@ export default function Component({ service }) {
|
|||||||
</Container>
|
</Container>
|
||||||
{enablePools &&
|
{enablePools &&
|
||||||
poolsData.map((pool) => (
|
poolsData.map((pool) => (
|
||||||
<Pool key={pool.id} name={pool.name} healthy={pool.healthy} allocated={pool.allocated} free={pool.free} />
|
<Pool
|
||||||
|
key={pool.id}
|
||||||
|
name={pool.name}
|
||||||
|
healthy={pool.healthy}
|
||||||
|
allocated={pool.allocated}
|
||||||
|
free={pool.free}
|
||||||
|
data={pool.data}
|
||||||
|
nasType={widget?.nasType ?? "scale"}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,18 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
export default function Pool({ name, free, allocated, healthy }) {
|
export default function Pool({ name, free, allocated, healthy, data, nasType }) {
|
||||||
const total = free + allocated;
|
let total = 0;
|
||||||
|
if (nasType === "scale") {
|
||||||
|
total = free + allocated;
|
||||||
|
} else {
|
||||||
|
allocated = 0; // eslint-disable-line no-param-reassign
|
||||||
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
|
total += data[i].stats.size;
|
||||||
|
allocated += data[i].stats.allocated; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const usedPercent = Math.round((allocated / total) * 100);
|
const usedPercent = Math.round((allocated / total) * 100);
|
||||||
const statusColor = healthy ? "bg-green-500" : "bg-yellow-500";
|
const statusColor = healthy ? "bg-green-500" : "bg-yellow-500";
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const widget = {
|
|||||||
healthy: entry.healthy,
|
healthy: entry.healthy,
|
||||||
allocated: entry.allocated,
|
allocated: entry.allocated,
|
||||||
free: entry.free,
|
free: entry.free,
|
||||||
|
data: entry.topology.data,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user