mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-25 10:29:47 -04:00
Enhancement: add additional fields to Tailscale Widget (#6589)
This commit is contained in:
parent
bf55e8acab
commit
d721bb661f
@ -9,7 +9,7 @@ You will need to generate an API access token from the [keys page](https://login
|
||||
|
||||
To find your device ID, go to the [machine overview page](https://login.tailscale.com/admin/machines) and select your machine. In the "Machine Details" section, copy your `ID`. It will end with `CNTRL`.
|
||||
|
||||
Allowed fields: `["address", "last_seen", "expires"]`.
|
||||
Allowed fields: `[ "address", "last_seen", "expires", "user", "hostname", "name", "client_version", "os", "created", "authorized", "is_external", "update_available", "tags" ]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
|
||||
@ -344,6 +344,16 @@
|
||||
"address": "Address",
|
||||
"expires": "Expires",
|
||||
"never": "Never",
|
||||
"user": "User",
|
||||
"hostname": "Hostname",
|
||||
"name": "Name",
|
||||
"client_version": "Client Version",
|
||||
"os": "OS",
|
||||
"created": "Created",
|
||||
"authorized": "Authorized",
|
||||
"is_external": "Is External",
|
||||
"update_available": "Update Available",
|
||||
"tags": "Tags",
|
||||
"last_seen": "Last Seen",
|
||||
"now": "Now",
|
||||
"years": "{{number}}y",
|
||||
@ -352,7 +362,9 @@
|
||||
"hours": "{{number}}h",
|
||||
"minutes": "{{number}}m",
|
||||
"seconds": "{{number}}s",
|
||||
"ago": "{{value}} Ago"
|
||||
"ago": "{{value}} Ago",
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
},
|
||||
"technitium": {
|
||||
"totalQueries": "Queries",
|
||||
|
||||
@ -9,13 +9,13 @@ export default function Component({ service }) {
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "device");
|
||||
const { data: tailscaleData, error: tailscaleError } = useWidgetAPI(widget, "device");
|
||||
|
||||
if (statsError || statsData?.message) {
|
||||
return <Container service={service} error={statsError ?? statsData} />;
|
||||
if (tailscaleError || tailscaleData?.message) {
|
||||
return <Container service={service} error={tailscaleError ?? tailscaleData} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
if (!tailscaleData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="tailscale.address" />
|
||||
@ -25,12 +25,29 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
if (widget.fields?.length == 0 || !widget.fields) {
|
||||
widget.fields = ["address", "last_seen", "expires"];
|
||||
} else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
const {
|
||||
addresses: [address],
|
||||
keyExpiryDisabled,
|
||||
lastSeen,
|
||||
expires,
|
||||
} = statsData;
|
||||
user,
|
||||
hostname,
|
||||
name,
|
||||
clientVersion,
|
||||
os,
|
||||
created,
|
||||
authorized,
|
||||
isExternal,
|
||||
updateAvailable,
|
||||
tags,
|
||||
} = tailscaleData;
|
||||
|
||||
const now = new Date();
|
||||
const compareDifferenceInTwoDates = (priorDate, futureDate) => {
|
||||
@ -62,11 +79,28 @@ export default function Component({ service }) {
|
||||
return compareDifferenceInTwoDates(now, date);
|
||||
};
|
||||
|
||||
const getBooleanAsString = (value) => {
|
||||
return value ? t("tailscale.true") : t("tailscale.false");
|
||||
};
|
||||
|
||||
const clientVersionString = clientVersion ? clientVersion.toString() : "-";
|
||||
const tagsString = tags && Array.isArray(tags) ? tags.join(", ") : "-";
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="tailscale.address" value={address} />
|
||||
<Block label="tailscale.last_seen" value={getLastSeen()} />
|
||||
<Block label="tailscale.expires" value={getExpiry()} />
|
||||
<Block label="tailscale.user" value={user} />
|
||||
<Block label="tailscale.hostname" value={hostname} />
|
||||
<Block label="tailscale.name" value={name} />
|
||||
<Block label="tailscale.client_version" value={clientVersionString} />
|
||||
<Block label="tailscale.os" value={os} />
|
||||
<Block label="tailscale.created" value={created} />
|
||||
<Block label="tailscale.authorized" value={getBooleanAsString(authorized)} />
|
||||
<Block label="tailscale.is_external" value={getBooleanAsString(isExternal)} />
|
||||
<Block label="tailscale.update_available" value={getBooleanAsString(updateAvailable)} />
|
||||
<Block label="tailscale.tags" value={tagsString} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,6 +22,23 @@ describe("widgets/tailscale/component", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const fullData = {
|
||||
addresses: ["127.0.0.1"],
|
||||
keyExpiryDisabled: false,
|
||||
expires: "2020-06-01T00:00:00Z",
|
||||
lastSeen: "2019-12-31T23:55:00Z",
|
||||
user: "fin@example.com",
|
||||
hostname: "localhost",
|
||||
name: "localhost.tail1234.ts.net",
|
||||
clientVersion: "1.1.0",
|
||||
os: "linux",
|
||||
created: "2019-06-01T00:00:00Z",
|
||||
authorized: true,
|
||||
isExternal: false,
|
||||
updateAvailable: true,
|
||||
tags: ["server", "prod"],
|
||||
};
|
||||
|
||||
it("renders placeholders while loading", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
@ -35,14 +52,108 @@ describe("widgets/tailscale/component", () => {
|
||||
expect(screen.getByText("tailscale.expires")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders address and expiry/last-seen strings when loaded", () => {
|
||||
describe("fields group: address, last_seen, expires, user", () => {
|
||||
it("renders only the specified 4 fields", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "user"] } }} />,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||
expectBlockValue(container, "tailscale.last_seen", "tailscale.ago");
|
||||
expectBlockValue(container, "tailscale.expires", "tailscale.weeks");
|
||||
expectBlockValue(container, "tailscale.user", "fin@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fields group: hostname, name, client_version, os", () => {
|
||||
it("renders only the specified 4 fields", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component service={{ widget: { type: "tailscale", fields: ["hostname", "name", "client_version", "os"] } }} />,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expectBlockValue(container, "tailscale.hostname", "localhost");
|
||||
expectBlockValue(container, "tailscale.name", "localhost.tail1234.ts.net");
|
||||
expectBlockValue(container, "tailscale.client_version", "1.1.0");
|
||||
expectBlockValue(container, "tailscale.os", "linux");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fields group: created, authorized, is_external, update_available", () => {
|
||||
it("renders only the specified 4 fields", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component
|
||||
service={{
|
||||
widget: { type: "tailscale", fields: ["created", "authorized", "is_external", "update_available"] },
|
||||
}}
|
||||
/>,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expectBlockValue(container, "tailscale.created", "2019-06-01T00:00:00Z");
|
||||
expectBlockValue(container, "tailscale.authorized", "tailscale.true");
|
||||
expectBlockValue(container, "tailscale.is_external", "tailscale.false");
|
||||
expectBlockValue(container, "tailscale.update_available", "tailscale.true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fields group: tags with defaults", () => {
|
||||
it("renders tags alongside default fields", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "tags"] } }} />,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||
expectBlockValue(container, "tailscale.tags", "server, prod");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fields truncation", () => {
|
||||
it("truncates to 4 fields when more than 4 are specified", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component
|
||||
service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "user", "hostname"] } }}
|
||||
/>,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expect(findServiceBlockByLabel(container, "tailscale.hostname")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("defaults to address, last_seen, expires when fields is empty", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(<Component service={{ widget: { type: "tailscale", fields: [] } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
|
||||
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||
expectBlockValue(container, "tailscale.last_seen", "tailscale.ago");
|
||||
expectBlockValue(container, "tailscale.expires", "tailscale.weeks");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders never for expires if key expiry is disabled", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
addresses: ["100.64.0.1"],
|
||||
keyExpiryDisabled: true,
|
||||
lastSeen: "2019-12-31T23:00:00Z",
|
||||
expires: "2021-01-01T00:00:00Z",
|
||||
},
|
||||
data: { ...fullData, keyExpiryDisabled: true },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
@ -50,8 +161,17 @@ describe("widgets/tailscale/component", () => {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expectBlockValue(container, "tailscale.address", "100.64.0.1");
|
||||
expect(findServiceBlockByLabel(container, "tailscale.last_seen")?.textContent).toContain("tailscale.ago");
|
||||
expectBlockValue(container, "tailscale.expires", "tailscale.never");
|
||||
});
|
||||
|
||||
it("renders error message when API returns an error", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "API error" } });
|
||||
|
||||
const { container } = renderWithProviders(<Component service={{ widget: { type: "tailscale" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(0);
|
||||
expect(container.textContent).toContain("API error");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user