mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-18 01:56:35 -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/swagdashboard.md
|
||||
- widgets/services/syncthing-relay-server.md
|
||||
- widgets/services/syncthing.md
|
||||
- widgets/services/tailscale.md
|
||||
- widgets/services/tandoor.md
|
||||
- widgets/services/technitium.md
|
||||
|
||||
@@ -1224,5 +1224,11 @@
|
||||
"burned": "Burned",
|
||||
"remaining": "Remaining",
|
||||
"steps": "Steps"
|
||||
},
|
||||
"syncthing": {
|
||||
"connected": "Connected",
|
||||
"synced": "Synced",
|
||||
"errors": "Errors",
|
||||
"storage": "Storage"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ const components = {
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||
syncthing: dynamic(() => import("./syncthing/component")),
|
||||
tailscale: dynamic(() => import("./tailscale/component")),
|
||||
tandoor: dynamic(() => import("./tandoor/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 suwayomi from "./suwayomi/widget";
|
||||
import swagdashboard from "./swagdashboard/widget";
|
||||
import syncthing from "./syncthing/widget";
|
||||
import tailscale from "./tailscale/widget";
|
||||
import tandoor from "./tandoor/widget";
|
||||
import tautulli from "./tautulli/widget";
|
||||
@@ -290,6 +291,7 @@ const widgets = {
|
||||
strelaysrv,
|
||||
swagdashboard,
|
||||
suwayomi,
|
||||
syncthing,
|
||||
tailscale,
|
||||
tandoor,
|
||||
tautulli,
|
||||
|
||||
Reference in New Issue
Block a user