mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-28 23:11:17 -07:00
Feature: Syncthing service widget (#6865)
Release Drafter / Auto Label PR (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Syncthing
|
||||||
|
description: Syncthing Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Syncthing](https://syncthing.net/).
|
||||||
|
|
||||||
|
Allowed fields: `["connected", "synced", "errors", "storage"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: syncthing
|
||||||
|
url: http://syncthing.host.or.ip
|
||||||
|
key: syncthingapikey
|
||||||
|
```
|
||||||
@@ -164,6 +164,7 @@ nav:
|
|||||||
- widgets/services/suwayomi.md
|
- widgets/services/suwayomi.md
|
||||||
- widgets/services/swagdashboard.md
|
- widgets/services/swagdashboard.md
|
||||||
- widgets/services/syncthing-relay-server.md
|
- widgets/services/syncthing-relay-server.md
|
||||||
|
- widgets/services/syncthing.md
|
||||||
- widgets/services/tailscale.md
|
- widgets/services/tailscale.md
|
||||||
- widgets/services/tandoor.md
|
- widgets/services/tandoor.md
|
||||||
- widgets/services/technitium.md
|
- widgets/services/technitium.md
|
||||||
|
|||||||
@@ -1224,5 +1224,11 @@
|
|||||||
"burned": "Burned",
|
"burned": "Burned",
|
||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"steps": "Steps"
|
"steps": "Steps"
|
||||||
|
},
|
||||||
|
"syncthing": {
|
||||||
|
"connected": "Connected",
|
||||||
|
"synced": "Synced",
|
||||||
|
"errors": "Errors",
|
||||||
|
"storage": "Storage"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ const components = {
|
|||||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||||
suwayomi: dynamic(() => import("./suwayomi/component")),
|
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||||
|
syncthing: dynamic(() => import("./syncthing/component")),
|
||||||
tailscale: dynamic(() => import("./tailscale/component")),
|
tailscale: dynamic(() => import("./tailscale/component")),
|
||||||
tandoor: dynamic(() => import("./tandoor/component")),
|
tandoor: dynamic(() => import("./tandoor/component")),
|
||||||
tautulli: dynamic(() => import("./tautulli/component")),
|
tautulli: dynamic(() => import("./tautulli/component")),
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next/pages";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data: connectionData, error: connectionError } = useWidgetAPI(widget, "connections");
|
||||||
|
const { data: completionData, error: completionError } = useWidgetAPI(widget, "completion");
|
||||||
|
const { data: errorData, error: errorError } = useWidgetAPI(widget, "error");
|
||||||
|
|
||||||
|
if (connectionError || completionError || errorError) {
|
||||||
|
return <Container service={service} error={connectionError ?? completionError ?? errorError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionData || !completionData || !errorData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="syncthing.connected" />
|
||||||
|
<Block label="syncthing.synced" />
|
||||||
|
<Block label="syncthing.errors" />
|
||||||
|
<Block label="syncthing.storage" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = Object.values(connectionData.connections);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block
|
||||||
|
label="syncthing.connected"
|
||||||
|
value={`${t("common.number", { value: connections.filter((c) => c.connected).length })} / ${t("common.number", { value: connections.length })}`}
|
||||||
|
/>
|
||||||
|
<Block label="syncthing.synced" value={t("common.percent", { value: completionData.completion })} />
|
||||||
|
<Block
|
||||||
|
label="syncthing.errors"
|
||||||
|
value={t("common.number", { value: errorData.errors ? errorData.errors.length : 0 })}
|
||||||
|
/>
|
||||||
|
<Block
|
||||||
|
label="syncthing.storage"
|
||||||
|
value={t("common.bytes", { value: completionData.globalBytes, maximumFractionDigits: 1, binary: true })}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { screen } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||||
|
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||||
|
|
||||||
|
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||||
|
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||||
|
|
||||||
|
import Component from "./component";
|
||||||
|
|
||||||
|
describe("widgets/syncthing/component", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders placeholders while loading", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "syncthing" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expect(screen.getByText("syncthing.connected")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("syncthing.synced")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("syncthing.storage")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("syncthing.errors")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders metrics when loaded", () => {
|
||||||
|
useWidgetAPI
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
data: {
|
||||||
|
connections: {
|
||||||
|
"test-1": { connected: true },
|
||||||
|
"test-2": { connected: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
data: {
|
||||||
|
completion: 75,
|
||||||
|
globalBytes: 1_234_567,
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
data: {
|
||||||
|
errors: ["err 1", "err 2"],
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "syncthing" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectBlockValue(container, "syncthing.connected", 1);
|
||||||
|
expectBlockValue(container, "syncthing.synced", 75);
|
||||||
|
expectBlockValue(container, "syncthing.errors", 2);
|
||||||
|
expectBlockValue(container, "syncthing.storage", "1234567");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["connections", "completion", "error"])("renders errors from the %s endpoint", (endpoint) => {
|
||||||
|
useWidgetAPI.mockImplementation((_widget, requestedEndpoint) => ({
|
||||||
|
data: undefined,
|
||||||
|
error: requestedEndpoint === endpoint ? `${endpoint} failed` : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "syncthing" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.textContent).toContain(`${endpoint} failed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders zero when there are no errors", () => {
|
||||||
|
useWidgetAPI
|
||||||
|
.mockReturnValueOnce({ data: { connections: {} }, error: undefined })
|
||||||
|
.mockReturnValueOnce({ data: { completion: 100, globalBytes: 0 }, error: undefined })
|
||||||
|
.mockReturnValueOnce({ data: {}, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "syncthing" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectBlockValue(container, "syncthing.errors", 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/rest/{endpoint}",
|
||||||
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
connections: {
|
||||||
|
endpoint: "system/connections",
|
||||||
|
validate: ["connections"],
|
||||||
|
},
|
||||||
|
completion: {
|
||||||
|
endpoint: "db/completion",
|
||||||
|
validate: ["completion", "globalBytes"],
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
endpoint: "system/error",
|
||||||
|
validate: ["errors"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||||
|
|
||||||
|
import widget from "./widget";
|
||||||
|
|
||||||
|
describe("syncthing widget config", () => {
|
||||||
|
it("exports a valid widget config", () => {
|
||||||
|
expectWidgetConfigShape(widget);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -128,6 +128,7 @@ import stocks from "./stocks/widget";
|
|||||||
import strelaysrv from "./strelaysrv/widget";
|
import strelaysrv from "./strelaysrv/widget";
|
||||||
import suwayomi from "./suwayomi/widget";
|
import suwayomi from "./suwayomi/widget";
|
||||||
import swagdashboard from "./swagdashboard/widget";
|
import swagdashboard from "./swagdashboard/widget";
|
||||||
|
import syncthing from "./syncthing/widget";
|
||||||
import tailscale from "./tailscale/widget";
|
import tailscale from "./tailscale/widget";
|
||||||
import tandoor from "./tandoor/widget";
|
import tandoor from "./tandoor/widget";
|
||||||
import tautulli from "./tautulli/widget";
|
import tautulli from "./tautulli/widget";
|
||||||
@@ -290,6 +291,7 @@ const widgets = {
|
|||||||
strelaysrv,
|
strelaysrv,
|
||||||
swagdashboard,
|
swagdashboard,
|
||||||
suwayomi,
|
suwayomi,
|
||||||
|
syncthing,
|
||||||
tailscale,
|
tailscale,
|
||||||
tandoor,
|
tandoor,
|
||||||
tautulli,
|
tautulli,
|
||||||
|
|||||||
Reference in New Issue
Block a user