Compare commits

..
10 changed files with 46 additions and 70 deletions

No files matched your search

-18
View File
@@ -46,24 +46,6 @@ jobs:
-
name: Check files
uses: pre-commit/action@v3.0.1
-
name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
-
name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
-
name: Install dependencies
run: pnpm install
-
name: Lint frontend
run: pnpm run lint
build:
name: Docker Build & Push
+8
View File
@@ -97,6 +97,10 @@ Each service can have multiple widgets attached to it, for example:
slug: statuspageslug
```
!!! note
Multiple widgets per service are not yet supported with Kubernetes ingress annotations.
#### Field Visibility
Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields.
@@ -178,6 +182,10 @@ To use a local icon, first create a Docker mount to `/app/public/icons` and then
Services may have an optional `ping` property that allows you to monitor the availability of an external host. As of v0.8.0, the ping feature attempts to use a true (ICMP) ping command on the underlying host. Currently, only IPv4 is supported.
!!! note
Because ping uses the ping command on the underlying host, in some cases you may need to install e.g. the `iputils-ping` package on the host system.
```yaml
- Group A:
- Sonarr:
+1 -1
View File
@@ -19,7 +19,7 @@ Allowed fields: `["uptime", "wan", "lan", "lan_users", "lan_devices", "wlan", "w
!!! hint
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container to clear the cache.
```yaml
widget:
+2 -2
View File
@@ -1,6 +1,6 @@
import { useTranslation } from "next-i18next";
import useSWR from "swr";
import { compareVersions, validate } from "compare-versions";
import { compareVersions } from "compare-versions";
import { MdNewReleases } from "react-icons/md";
export default function Version() {
@@ -44,7 +44,7 @@ export default function Version() {
</a>
)}
</span>
{version === "main" || version === "dev" || version === "nightly" || !validate(version)
{version === "main" || version === "dev" || version === "nightly"
? null
: releaseData &&
latestRelease &&
-5
View File
@@ -67,8 +67,3 @@ dialog ::-webkit-scrollbar {
.chart + .chart {
margin-top: 2em;
}
.service-container + .chart {
margin-top: 2.5rem;
margin-bottom: .5rem;
}
+1 -1
View File
@@ -79,7 +79,7 @@ export default async function genericProxyHandler(req, res, map) {
error: {
message: "HTTP Error",
url: sanitizeErrorURL(url),
data: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
},
});
}
+1 -4
View File
@@ -29,13 +29,10 @@ export default function Component({ service }) {
);
}
// broken by evcc v0.133.0 https://github.com/evcc-io/evcc/commit/9dcb1fa0a7c08dd926b79309aa1f676a5fc6c8aa
const gridPower = stateData.result.gridPower ?? stateData.result.grid?.power ?? 0;
return (
<Container service={service}>
<Block label="evcc.pv_power" value={`${toKilowatts(t, stateData.result.pvPower)} ${t("evcc.kilowatt")}`} />
<Block label="evcc.grid_power" value={`${toKilowatts(t, gridPower)} ${t("evcc.kilowatt")}`} />
<Block label="evcc.grid_power" value={`${toKilowatts(t, stateData.result.gridPower)} ${t("evcc.kilowatt")}`} />
<Block label="evcc.home_power" value={`${toKilowatts(t, stateData.result.homePower)} ${t("evcc.kilowatt")}`} />
<Block
label="evcc.charge_power"
+15 -23
View File
@@ -12,15 +12,14 @@ 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, widget?.enablePools ? "pools" : null);
const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : null);
const { data: poolsData, error: poolsError } = useWidgetAPI(widget, "pools");
if (alertError || statusError || poolsError) {
const finalError = alertError ?? statusError ?? poolsError ?? datasetError;
const finalError = alertError ?? statusError ?? poolsError;
return <Container service={service} error={finalError} />;
}
if (!alertData || !statusData || (widget?.enablePools && (!poolsData || !datasetData))) {
if (!alertData || !statusData) {
return (
<Container service={service}>
<Block label="truenas.load" />
@@ -30,22 +29,7 @@ export default function Component({ service }) {
);
}
let pools = [];
const showPools =
Array.isArray(poolsData) && poolsData.length > 0 && Array.isArray(datasetData) && datasetData.length > 0;
if (showPools) {
pools = poolsData.map((pool) => {
const dataset = datasetData.find((d) => d.pool === pool.name && d.name === pool.name);
return {
id: pool.id,
name: pool.name,
healthy: pool.healthy,
allocated: dataset?.used.parsed ?? 0,
free: dataset?.available.parsed ?? 0,
};
});
}
const enablePools = widget?.enablePools && Array.isArray(poolsData) && poolsData.length > 0;
return (
<>
@@ -54,11 +38,19 @@ export default function Component({ service }) {
<Block label="truenas.uptime" value={t("common.duration", { value: statusData.uptime_seconds })} />
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
</Container>
{showPools &&
pools
{enablePools &&
poolsData
.sort((a, b) => a.name.localeCompare(b.name))
.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"}
/>
))}
</>
);
+15 -13
View File
@@ -1,9 +1,19 @@
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import prettyBytes from "pretty-bytes";
export default function Pool({ name, free, allocated, healthy }) {
const { t } = useTranslation();
const usedPercent = Math.round((allocated / (free + allocated)) * 100);
export default function Pool({ name, free, allocated, healthy, data, nasType }) {
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 statusColor = healthy ? "bg-green-500" : "bg-yellow-500";
return (
@@ -22,15 +32,7 @@ export default function Pool({ name, free, allocated, healthy }) {
</div>
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
<span>
{`${t("common.bytes", {
value: allocated,
maximumFractionDigits: 1,
binary: true,
})} / ${t("common.bytes", {
value: free + allocated,
maximumFractionDigits: 1,
binary: true,
})}`}
{prettyBytes(allocated)} / {prettyBytes(total)}
</span>
<span className="pl-2">({usedPercent}%)</span>
</div>
+3 -3
View File
@@ -23,11 +23,11 @@ const widget = {
id: entry.name,
name: entry.name,
healthy: entry.healthy,
allocated: entry.allocated,
free: entry.free,
data: entry.topology?.data ?? [],
})),
},
dataset: {
endpoint: "pool/dataset",
},
},
};